mpin add and 2026-04-19 updates

This commit is contained in:
MAGESHWARAN 2026-04-19 21:44:01 +05:30
parent 41d7cfa8c6
commit 7d25e77138
15 changed files with 2555 additions and 1643 deletions

View File

@ -1,283 +1,424 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.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:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/app_asstes.dart'; import 'package:get/get.dart';
import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_style.dart'; import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/app_style.dart';
import 'package:taxglide/consts/comman_container_auth.dart'; import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/consts/comman_textformfileds.dart'; import 'package:taxglide/consts/comman_container_auth.dart';
import 'package:taxglide/consts/responsive_helper.dart'; import 'package:taxglide/consts/comman_textformfileds.dart';
import 'package:taxglide/consts/validation_popup.dart'; import 'package:taxglide/consts/responsive_helper.dart';
import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/consts/validation_popup.dart';
import 'package:taxglide/router/consts_routers.dart'; import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/router/consts_routers.dart';
class EmployeeLoginScreen extends ConsumerStatefulWidget { import 'package:taxglide/view/Main_controller/main_controller.dart';
const EmployeeLoginScreen({super.key});
class EmployeeLoginScreen extends ConsumerStatefulWidget {
@override const EmployeeLoginScreen({super.key});
ConsumerState<EmployeeLoginScreen> createState() =>
_EmployeeLoginScreenState(); @override
} ConsumerState<EmployeeLoginScreen> createState() =>
_EmployeeLoginScreenState();
class _EmployeeLoginScreenState extends ConsumerState<EmployeeLoginScreen> { }
final ValidationPopup _validationPopup = ValidationPopup();
final TextEditingController _mobileController = TextEditingController(); class _EmployeeLoginScreenState extends ConsumerState<EmployeeLoginScreen> {
final ValidationPopup _validationPopup = ValidationPopup();
@override final TextEditingController _emailController = TextEditingController();
void initState() {
super.initState(); // Multi-box PIN logic
final args = Get.arguments; final int pinLength = 4;
if (args != null && args is Map<String, dynamic>) { final List<TextEditingController> _pinControllers = [];
final mobile = args['mobile'] ?? ''; final List<FocusNode> _pinFocusNodes = [];
if (mobile.isNotEmpty) { String pinValue = '';
_mobileController.text = mobile;
} // Error states
} bool _emailHasError = false;
} bool _pinHasError = false;
@override @override
void dispose() { void initState() {
_mobileController.dispose(); super.initState();
super.dispose(); // Initialize PIN controllers and focus nodes
} for (int i = 0; i < pinLength; i++) {
_pinControllers.add(TextEditingController());
Future<void> _handleLogin() async { _pinFocusNodes.add(FocusNode());
final mobile = _mobileController.text.trim(); }
if (!_validationPopup.validateMobileNumber(context, mobile)) { final args = Get.arguments;
return; if (args != null && args is Map<String, dynamic>) {
} final email = args['email'] ?? '';
if (email.isNotEmpty) {
await ref.read(employeeloginProvider.notifier).login(mobile); _emailController.text = email;
final state = ref.read(employeeloginProvider); }
}
state.when( }
data: (data) {
if (data['success'] == true) { @override
Get.toNamed(ConstRouters.employeeotp, arguments: {'mobile': mobile}); void dispose() {
_validationPopup.showSuccessMessage( _emailController.dispose();
context, for (var controller in _pinControllers) {
"OTP sent successfully!", controller.dispose();
); }
} else if (data['error'] != null) { for (var node in _pinFocusNodes) {
_validationPopup.showErrorMessage(context, data['error'].toString()); node.dispose();
} }
}, super.dispose();
loading: () {}, }
error: (err, _) {
_validationPopup.showErrorMessage(context, "Error: $err"); void _onPinChange(int index, String value) {
}, if (_pinHasError) {
); setState(() => _pinHasError = false);
} }
if (value.isNotEmpty && index < pinLength - 1) {
@override _pinFocusNodes[index + 1].requestFocus();
Widget build(BuildContext context) { }
final loginState = ref.watch(employeeloginProvider); if (value.isEmpty && index > 0) {
_pinFocusNodes[index - 1].requestFocus();
// Initialize responsive utils }
final r = ResponsiveUtils(context); setState(() {
pinValue = _pinControllers.map((c) => c.text).join();
// Responsive values });
final logoWidth = r.getValue<double>( }
mobile: 120,
tablet: 141, Future<void> _handleLogin() async {
desktop: 160, final email = _emailController.text.trim();
); final pin = pinValue.trim();
final logoHeight = r.getValue<double>(
mobile: 85, setState(() {
tablet: 100, _emailHasError = false;
desktop: 115, _pinHasError = false;
); });
final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); if (!_validationPopup.validateEmail(context, email)) {
final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12); setState(() => _emailHasError = true);
final linkFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); return;
final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); }
if (!_validationPopup.validatePin(context, pin)) {
final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); setState(() => _pinHasError = true);
final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24); return;
final spacingMD = r.spacing(mobile: 20, tablet: 20, desktop: 24); }
final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28);
await ref.read(employeeloginProvider.notifier).loginWithPin(email, pin);
return PopScope( final state = ref.read(employeeloginProvider);
canPop: false,
onPopInvokedWithResult: (didPop, result) { state.when(
if (didPop) return; data: (data) {
Get.offAllNamed(ConstRouters.login); if (data['success'] == true) {
}, _validationPopup.showSuccessMessage(
child: Scaffold( context,
resizeToAvoidBottomInset: true, "Login Successful!",
body: Container( );
width: double.infinity, Future.delayed(const Duration(seconds: 1), () {
height: double.infinity, Get.offAll(() => const MainController());
decoration: const BoxDecoration( });
gradient: LinearGradient( } else if (data['error'] != null) {
begin: Alignment.topLeft, _validationPopup.showErrorMessage(context, data['error'].toString());
end: Alignment.bottomRight, setState(() {
colors: [ _emailHasError = true;
Color(0xFFFFF8F0), _pinHasError = true;
Color(0xFFEBC894), });
Color(0xFFE8DAF2), }
Color(0xFFB49EF4), },
], loading: () {},
stops: [0.0, 0.3, 0.6, 1.0], error: (err, _) {
), _validationPopup.showErrorMessage(context, "Error: $err");
), },
child: SingleChildScrollView( );
child: ConstrainedBox( }
constraints: BoxConstraints(minHeight: r.screenHeight),
child: IntrinsicHeight( @override
child: Center( Widget build(BuildContext context) {
child: CommonContainerAuth( final loginState = ref.watch(employeeloginProvider);
child: Column(
mainAxisSize: MainAxisSize.min, // Initialize responsive utils
crossAxisAlignment: CrossAxisAlignment.center, final r = ResponsiveUtils(context);
children: [
// Logo // Responsive values
Image.asset( final logoWidth = r.getValue<double>(mobile: 120, tablet: 141, desktop: 160);
AppAssets.taxgildelogoauth, final logoHeight = r.getValue<double>(mobile: 85, tablet: 100, desktop: 115);
width: logoWidth, final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
height: logoHeight, final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
fit: BoxFit.contain, final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12);
), final linkFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
SizedBox(height: spacingMD), final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14);
// Login Title final pinBoxWidth = r.getValue<double>(mobile: 55, tablet: 60, desktop: 68);
Text( final pinBoxHeight = r.getValue<double>(mobile: 55, tablet: 60, desktop: 68);
"Login", final pinFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32);
textAlign: TextAlign.center, final pinBoxMargin = r.getValue<double>(mobile: 8, tablet: 6, desktop: 8);
style: AppTextStyles.bold.copyWith( final pinBoxRadius = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
fontSize: titleFontSize,
height: 1.3, final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12);
letterSpacing: 0.01 * titleFontSize, final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24);
color: AppColors.authheading, final spacingMD = r.spacing(mobile: 20, tablet: 20, desktop: 24);
), final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28);
),
SizedBox(height: spacingMD), return PopScope(
canPop: false,
// Subtitle onPopInvokedWithResult: (didPop, result) {
Text( if (didPop) return;
"Enter your registered Mobile Number", Get.offAllNamed(ConstRouters.login);
textAlign: TextAlign.center, },
style: AppTextStyles.bold.copyWith( child: Scaffold(
fontSize: subtitleFontSize, resizeToAvoidBottomInset: true,
height: 1.4, body: Container(
letterSpacing: 0.03 * subtitleFontSize, width: double.infinity,
color: AppColors.authleading, height: double.infinity,
), decoration: const BoxDecoration(
), gradient: LinearGradient(
SizedBox(height: spacingLG), begin: Alignment.topLeft,
end: Alignment.bottomRight,
// Mobile Number Input colors: [
CommanTextFormField( Color(0xFFFFF8F0),
controller: _mobileController, Color(0xFFEBC894),
hintText: 'Enter your Mobile Number', Color(0xFFE8DAF2),
keyboardType: TextInputType.phone, Color(0xFFB49EF4),
prefixIcon: Icons.phone_android, ],
), stops: [0.0, 0.3, 0.6, 1.0],
SizedBox(height: spacingLG), ),
),
// Terms and Conditions child: SingleChildScrollView(
Text.rich( child: ConstrainedBox(
TextSpan( constraints: BoxConstraints(minHeight: r.screenHeight),
text: "By signing up, you agree to the ", child: IntrinsicHeight(
style: AppTextStyles.medium.copyWith( child: Center(
fontSize: termsFontSize, child: CommonContainerAuth(
height: 1.8, child: Column(
letterSpacing: 0.01, mainAxisSize: MainAxisSize.min,
color: AppColors.authleading, crossAxisAlignment: CrossAxisAlignment.center,
), children: [
children: [ // Logo
TextSpan( Image.asset(
text: "Terms of Service ", AppAssets.taxgildelogoauth,
style: AppTextStyles.bold.copyWith( width: logoWidth,
fontSize: termsFontSize, height: logoHeight,
height: 1.8, fit: BoxFit.contain,
letterSpacing: 0.01, ),
color: AppColors.authtermsandcondition, SizedBox(height: spacingMD),
),
), // Login Title
TextSpan( Text(
text: "and ", "Employee Login",
style: AppTextStyles.medium.copyWith( textAlign: TextAlign.center,
fontSize: termsFontSize, style: AppTextStyles.bold.copyWith(
height: 1.8, fontSize: titleFontSize,
letterSpacing: 0.01, color: AppColors.authheading,
color: AppColors.authleading, ),
), ),
), SizedBox(height: spacingMD),
TextSpan(
text: "Data Processing Agreement", // Subtitle
style: AppTextStyles.bold.copyWith( Text(
fontSize: termsFontSize, "Enter your Email and PIN",
height: 1.8, textAlign: TextAlign.center,
letterSpacing: 0.01, style: AppTextStyles.bold.copyWith(
color: AppColors.authtermsandcondition, fontSize: subtitleFontSize,
), color: AppColors.authleading,
), ),
], ),
), SizedBox(height: spacingLG),
textAlign: TextAlign.center,
), // Email Input
CommanTextFormField(
SizedBox(height: spacingLG), controller: _emailController,
hintText: 'Enter your Email',
// Login Button or Loading keyboardType: TextInputType.emailAddress,
loginState.isLoading prefixIcon: Icons.email_outlined,
? const CircularProgressIndicator() hasError: _emailHasError,
: CommanButton( onChanged: (value) {
text: "Login", if (_emailHasError) {
onPressed: _handleLogin, setState(() => _emailHasError = false);
), }
},
SizedBox(height: spacingSM), ),
SizedBox(height: spacingMD),
Text(
"Other Login", // PIN Boxes
textAlign: TextAlign.center, Row(
style: AppTextStyles.semiBold.copyWith( mainAxisAlignment: MainAxisAlignment.center,
fontSize: otherLoginFontSize, children: List.generate(pinLength, (index) {
height: 1.8, bool isFilled = _pinControllers[index].text.isNotEmpty;
letterSpacing: 0.13, return Container(
color: AppColors.authleading, margin: EdgeInsets.symmetric(horizontal: pinBoxMargin),
), width: pinBoxWidth,
), height: pinBoxHeight,
decoration: BoxDecoration(
SizedBox(height: spacingXS), color: isFilled ? AppColors.commanbutton : Colors.white,
borderRadius: BorderRadius.circular(pinBoxRadius),
Text.rich( border: Border.all(
TextSpan( color: _pinHasError
text: "User Login? ", ? Colors.red
style: AppTextStyles.bold.copyWith( : const Color(0xFFDFDFDF),
fontSize: linkFontSize, width: _pinHasError ? 1.5 : 1.0,
color: AppColors.black, ),
), boxShadow: [
children: [ BoxShadow(
TextSpan( color: _pinHasError
text: "Click Here", ? Colors.red.withOpacity(0.1)
style: AppTextStyles.bold.copyWith( : const Color(0xFFBDBDBD).withOpacity(0.25),
fontSize: linkFontSize, blurRadius: 7,
color: AppColors.authsignup, offset: const Offset(0, 1),
), ),
recognizer: TapGestureRecognizer() ],
..onTap = () { ),
Get.offNamed(ConstRouters.login); 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);
},
),
],
),
),
],
),
),
),
),
),
),
),
),
);
}
}

View File

@ -1,420 +1,342 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/consts/comman_container_auth.dart'; import 'package:taxglide/consts/comman_container_auth.dart';
import 'package:taxglide/consts/responsive_helper.dart'; import 'package:taxglide/consts/responsive_helper.dart';
import 'package:taxglide/consts/validation_popup.dart'; import 'package:taxglide/consts/validation_popup.dart';
import 'package:taxglide/controller/api_contoller.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:flutter/services.dart';
import '../router/consts_routers.dart'; import 'package:taxglide/consts/app_style.dart';
class EmployeeOtpScreen extends ConsumerStatefulWidget { class EmployeeOtpScreen extends ConsumerStatefulWidget {
const EmployeeOtpScreen({super.key}); const EmployeeOtpScreen({super.key});
@override @override
ConsumerState<EmployeeOtpScreen> createState() => _EmployeeOtpScreenState(); ConsumerState<EmployeeOtpScreen> createState() => _EmployeeOtpScreenState();
} }
class _EmployeeOtpScreenState extends ConsumerState<EmployeeOtpScreen> { class _EmployeeOtpScreenState extends ConsumerState<EmployeeOtpScreen> {
final ValidationPopup _validationPopup = ValidationPopup(); final ValidationPopup _validationPopup = ValidationPopup();
final int otpLength = 4; final int otpLength = 4;
final List<TextEditingController> _controllers = []; final List<TextEditingController> _controllers = [];
final List<FocusNode> _focusNodes = []; final List<FocusNode> _focusNodes = [];
String otpValue = ''; String otpValue = '';
Timer? _timer; Timer? _timer;
int _remainingSeconds = 600; int _remainingSeconds = 600;
bool _canResend = false; bool _canResend = false;
late String mobile; late String _identifier;
bool _fromForgot = false;
@override bool _isEmployee = true;
void initState() {
super.initState(); @override
final args = Get.arguments as Map<String, dynamic>?; void initState() {
mobile = args?['mobile'] ?? ''; super.initState();
final args = Get.arguments as Map<String, dynamic>? ?? {};
for (int i = 0; i < otpLength; i++) { _fromForgot = args['fromForgot'] ?? false;
_controllers.add(TextEditingController()); _identifier = args['email'] ?? args['mobile'] ?? '';
_focusNodes.add(FocusNode());
} for (int i = 0; i < otpLength; i++) {
_startTimer(); _controllers.add(TextEditingController());
} _focusNodes.add(FocusNode());
}
@override _startTimer();
void dispose() { }
_timer?.cancel();
for (var c in _controllers) c.dispose(); @override
for (var f in _focusNodes) f.dispose(); void dispose() {
super.dispose(); _timer?.cancel();
} for (var c in _controllers) c.dispose();
for (var f in _focusNodes) f.dispose();
void _startTimer() { super.dispose();
_canResend = false; }
_remainingSeconds = 30;
_timer?.cancel(); void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { _canResend = false;
if (_remainingSeconds > 0) { _remainingSeconds = 30;
setState(() => _remainingSeconds--); _timer?.cancel();
} else { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() => _canResend = true); if (_remainingSeconds > 0) {
timer.cancel(); 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')}'; String _formatTime(int seconds) {
} int minutes = seconds ~/ 60;
int remainingSeconds = seconds % 60;
Future<void> _resendOtp() async { return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
if (!_canResend) return; }
for (var controller in _controllers) controller.clear(); void _resendOtp() async {
setState(() => otpValue = ''); if (!_canResend) return;
_focusNodes[0].requestFocus(); for (var controller in _controllers) controller.clear();
setState(() => otpValue = '');
try { _focusNodes[0].requestFocus();
await ref.read(employeeloginProvider.notifier).login(mobile);
final loginState = ref.read(employeeloginProvider); if (_fromForgot) {
loginState.when( await ref.read(forgotPasswordProvider.notifier).requestOtp(_identifier, _isEmployee);
data: (result) { final state = ref.read(forgotPasswordProvider);
if (result['success'] == true) { state.when(
_validationPopup.showSuccessMessage( data: (result) {
context, if (result['success'] == true) {
"OTP has been resent successfully!", _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!");
); _startTimer();
_startTimer(); } else {
} else { _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP");
_validationPopup.showErrorMessage( }
context, },
result['error'] ?? "Failed to resend OTP", loading: () {},
); error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"),
} );
}, } else {
loading: () {}, await ref.read(employeeloginProvider.notifier).login(_identifier);
error: (error, _) { final state = ref.read(employeeloginProvider);
_validationPopup.showErrorMessage( state.when(
context, data: (result) {
"Failed to resend OTP. Please try again.", if (result['success'] == true) {
); _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!");
}, _startTimer();
); } else {
} catch (e) { _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP");
_validationPopup.showErrorMessage( }
context, },
"An error occurred. Please try again.", loading: () {},
); error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"),
} );
} }
}
Future<void> _verifyOtp() async {
if (otpValue.length != otpLength) { void _verifyOtp() async {
_validationPopup.showErrorMessage(context, "Please enter all OTP digits"); if (otpValue.length != otpLength) {
return; _validationPopup.showErrorMessage(context, "Please enter all OTP digits");
} return;
}
await ref.read(employeeloginProvider.notifier).verifyOtp(mobile, otpValue);
final loginState = ref.read(employeeloginProvider); if (_fromForgot) {
await ref.read(forgotPasswordProvider.notifier).verifyOtp(_identifier, otpValue);
loginState.when( final state = ref.read(forgotPasswordProvider);
data: (result) { state.when(
if (result['success'] == true) { data: (result) {
_validationPopup.showSuccessMessage( if (result['success'] == true) {
context, _validationPopup.showSuccessMessage(context, "OTP Verified successfully!");
"OTP Verified Successfully!", Get.toNamed(ConstRouters.mpinSet);
); } else {
Future.delayed(const Duration(seconds: 1), () { _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP");
Get.offAll(() => const MainController()); }
}); },
} else { loading: () {},
_validationPopup.showErrorMessage( error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"),
context, );
result['error'] ?? "Invalid OTP. Please try again.", } else {
); await ref.read(employeeloginProvider.notifier).verifyOtp(_identifier, otpValue);
} final state = ref.read(employeeloginProvider);
}, state.when(
loading: () {}, data: (result) {
error: (error, _) { if (result['success'] == true) {
_validationPopup.showErrorMessage( _validationPopup.showSuccessMessage(context, "OTP Verified successfully!");
context, Get.offAllNamed(ConstRouters.mpinSet);
"Failed to verify OTP. Please try again.", } 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(); }
} else if (value.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus(); void _onOtpChange(int index, String value) {
} if (value.isNotEmpty && index < otpLength - 1) {
setState(() { _focusNodes[index + 1].requestFocus();
otpValue = _controllers.map((c) => c.text).join(); }
}); if (value.isEmpty && index > 0) {
} _focusNodes[index - 1].requestFocus();
}
@override setState(() {
Widget build(BuildContext context) { otpValue = _controllers.map((c) => c.text).join();
final loginState = ref.watch(employeeloginProvider); });
final isLoading = loginState.isLoading; }
// Initialize responsive utils @override
final r = ResponsiveUtils(context); Widget build(BuildContext context) {
final isLoading = _fromForgot
// Responsive values ? ref.watch(forgotPasswordProvider).isLoading
final logoWidth = r.getValue<double>( : ref.watch(employeeloginProvider).isLoading;
mobile: 120,
tablet: 141, final r = ResponsiveUtils(context);
desktop: 160,
); final logoWidth = r.getValue<double>(mobile: 120, tablet: 141, desktop: 160);
final logoHeight = r.getValue<double>( final logoHeight = r.getValue<double>(mobile: 85, tablet: 100, desktop: 115);
mobile: 85, final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
tablet: 100, final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
desktop: 115, final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
); final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18);
final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); final otpBoxWidth = r.getValue<double>(mobile: 52, tablet: 60, desktop: 68);
final mobileFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); final otpBoxHeight = r.getValue<double>(mobile: 48, tablet: 56, desktop: 64);
final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32);
final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); final otpBoxMargin = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
final otpBoxRadius = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
final otpBoxWidth = r.getValue<double>(mobile: 52, tablet: 60, desktop: 68);
final otpBoxHeight = r.getValue<double>( final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24);
mobile: 48, final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26);
tablet: 56, final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36);
desktop: 64,
); return Scaffold(
final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); body: Stack(
final otpBoxMargin = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8); children: [
final otpBoxRadius = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8); Container(
decoration: const BoxDecoration(
final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); gradient: LinearGradient(
final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); begin: Alignment.topLeft,
final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); end: Alignment.bottomRight,
final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); colors: [
Color(0xFFFFF8F0),
return Scaffold( Color(0xFFEBC894),
body: Stack( Color(0xFFE8DAF2),
children: [ Color(0xFFB49EF4),
// Gradient background ],
Container( stops: [0.0, 0.3, 0.6, 1.0],
decoration: const BoxDecoration( ),
gradient: LinearGradient( ),
begin: Alignment.topLeft, ),
end: Alignment.bottomRight, SingleChildScrollView(
colors: [ child: SizedBox(
Color(0xFFFFF8F0), height: r.screenHeight,
Color(0xFFEBC894), child: Center(
Color(0xFFE8DAF2), child: CommonContainerAuth(
Color(0xFFB49EF4), child: Column(
], mainAxisSize: MainAxisSize.min,
stops: [0.0, 0.3, 0.6, 1.0], children: [
), Image.asset(
), AppAssets.taxgildelogoauth,
), width: logoWidth,
height: logoHeight,
// Scrollable content fit: BoxFit.contain,
SingleChildScrollView( ),
child: SizedBox( SizedBox(height: spacingSM),
height: r.screenHeight, Text(
child: Center( "Enter Staff OTP",
child: CommonContainerAuth( style: AppTextStyles.bold.copyWith(
child: Column( fontSize: titleFontSize,
mainAxisSize: MainAxisSize.min, color: AppColors.authheading,
crossAxisAlignment: CrossAxisAlignment.center, ),
children: [ ),
// Logo SizedBox(height: spacingSM),
Image.asset( Text(
AppAssets.taxgildelogoauth, "OTP has been sent to ${_identifier}",
width: logoWidth, textAlign: TextAlign.center,
height: logoHeight, style: AppTextStyles.medium.copyWith(
fit: BoxFit.contain, fontSize: subtitleFontSize,
), color: AppColors.authleading,
SizedBox(height: spacingSM), ),
),
// Title SizedBox(height: spacingMD),
Text( Row(
"Enter OTP", mainAxisAlignment: MainAxisAlignment.center,
textAlign: TextAlign.center, children: List.generate(otpLength, (index) {
style: TextStyle( bool isFilled = _controllers[index].text.isNotEmpty;
fontFamily: "Gilroy", return Container(
fontSize: titleFontSize, margin: EdgeInsets.symmetric(horizontal: otpBoxMargin),
fontWeight: FontWeight.w600, width: otpBoxWidth,
height: 1.3, height: otpBoxHeight,
letterSpacing: 0.01 * titleFontSize, decoration: BoxDecoration(
color: AppColors.authheading, color: isFilled ? AppColors.commanbutton : Colors.white,
), borderRadius: BorderRadius.circular(otpBoxRadius),
), border: Border.all(color: const Color(0xFFDFDFDF)),
SizedBox(height: spacingSM), boxShadow: [
BoxShadow(
// Subtitle color: const Color(0xFFBDBDBD).withOpacity(0.25),
Text( blurRadius: 7,
"OTP has been sent to your registered mobile number", offset: const Offset(0, 1),
textAlign: TextAlign.center, ),
style: TextStyle( ],
fontFamily: "Gilroy", ),
fontSize: subtitleFontSize, child: Center(
fontWeight: FontWeight.w600, child: KeyboardListener(
height: 1.4, focusNode: FocusNode(),
letterSpacing: 0.03 * subtitleFontSize, onKeyEvent: (event) {
color: AppColors.authleading, if (event is KeyDownEvent &&
), event.logicalKey ==
), LogicalKeyboardKey.backspace) {
SizedBox(height: spacingMD), if (_controllers[index].text.isEmpty &&
index > 0) {
// Mobile + Change Number _focusNodes[index - 1].requestFocus();
Text.rich( }
TextSpan( }
text: mobile, },
style: TextStyle( child: TextField(
fontFamily: "Gilroy", controller: _controllers[index],
fontWeight: FontWeight.w500, focusNode: _focusNodes[index],
fontSize: mobileFontSize, textAlign: TextAlign.center,
height: 1.8, keyboardType: TextInputType.number,
letterSpacing: 0.01, maxLength: 1,
color: AppColors.authleading, enabled: !isLoading,
), style: TextStyle(
children: [ fontFamily: "Gilroy",
TextSpan( fontWeight: FontWeight.w400,
text: " ( Change Number )", fontSize: otpFontSize,
style: TextStyle( color: isFilled ? Colors.white : Colors.black,
fontFamily: "Gilroy", ),
fontWeight: FontWeight.w800, decoration: const InputDecoration(
fontSize: mobileFontSize, counterText: '',
height: 1.8, border: InputBorder.none,
letterSpacing: 0.04, ),
color: AppColors.authchange, onChanged: (value) =>
), _onOtpChange(index, value),
recognizer: TapGestureRecognizer() ),
..onTap = () { ),
Get.offNamed( ),
ConstRouters.employeelogin, );
arguments: {'mobile': mobile}, }),
); ),
}, SizedBox(height: spacingSM),
), Align(
], alignment: Alignment.centerRight,
), child: GestureDetector(
textAlign: TextAlign.center, onTap: _canResend && !isLoading ? _resendOtp : null,
), child: Text(
SizedBox(height: spacingLG), "Resend",
style: AppTextStyles.medium.copyWith(
// OTP Input Fields fontSize: resendFontSize,
Row( color: _canResend && !isLoading
mainAxisAlignment: MainAxisAlignment.center, ? AppColors.authchange
children: List.generate(otpLength, (index) { : Colors.grey,
bool isFilled = _controllers[index].text.isNotEmpty; ),
return Container( ),
margin: EdgeInsets.symmetric( ),
horizontal: otpBoxMargin, ),
), SizedBox(height: spacingSM),
width: otpBoxWidth, Text(
height: otpBoxHeight, _formatTime(_remainingSeconds),
decoration: BoxDecoration( style: AppTextStyles.semiBold.copyWith(
color: isFilled fontSize: timerFontSize,
? AppColors.commanbutton color: _remainingSeconds > 0 ? AppColors.authheading : Colors.red,
: Colors.white, ),
borderRadius: BorderRadius.circular(otpBoxRadius), ),
border: Border.all( SizedBox(height: spacingLG),
color: const Color(0xFFDFDFDF), isLoading
), ? const CircularProgressIndicator()
boxShadow: [ : CommanButton(text: "Verify", onPressed: _verifyOtp),
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),
],
),
),
),
),
),
],
),
);
}
}

168
lib/auth/forgot_screen.dart Normal file
View File

@ -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<ForgotScreen> createState() => _ForgotScreenState();
}
class _ForgotScreenState extends ConsumerState<ForgotScreen> {
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<String, dynamic>) {
_isEmployee = args['isEmployee'] ?? false;
}
}
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
Future<void> _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<double>(mobile: 120, tablet: 141, desktop: 160);
final logoHeight = r.getValue<double>(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,
),
),
),
],
),
),
),
),
),
);
}
}

View File

@ -1,351 +1,453 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.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:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/app_asstes.dart'; import 'package:get/get.dart';
import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/consts/comman_container_auth.dart'; import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/consts/comman_textformfileds.dart'; import 'package:taxglide/consts/comman_container_auth.dart';
import 'package:taxglide/consts/comman_popup.dart'; import 'package:taxglide/consts/comman_textformfileds.dart';
import 'package:taxglide/consts/responsive_helper.dart'; import 'package:taxglide/consts/comman_popup.dart';
import 'package:taxglide/consts/validation_popup.dart'; import 'package:taxglide/consts/responsive_helper.dart';
import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/consts/validation_popup.dart';
import 'package:taxglide/router/consts_routers.dart'; import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/consts/app_style.dart'; import 'package:taxglide/router/consts_routers.dart';
import 'package:taxglide/consts/app_style.dart';
class LoginScreen extends ConsumerStatefulWidget { import 'package:taxglide/view/Main_controller/main_controller.dart';
const LoginScreen({super.key});
class LoginScreen extends ConsumerStatefulWidget {
@override const LoginScreen({super.key});
ConsumerState<LoginScreen> createState() => _LoginScreenState();
} @override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
class _LoginScreenState extends ConsumerState<LoginScreen> { }
final TextEditingController _mobileController = TextEditingController();
final ValidationPopup _validationPopup = ValidationPopup(); class _LoginScreenState extends ConsumerState<LoginScreen> {
final TextEditingController _emailController = TextEditingController();
@override final ValidationPopup _validationPopup = ValidationPopup();
void initState() {
super.initState(); // Multi-box PIN logic
final args = Get.arguments; final int pinLength = 4;
if (args != null && args is Map<String, dynamic>) { final List<TextEditingController> _pinControllers = [];
final mobile = args['mobile'] ?? ''; final List<FocusNode> _pinFocusNodes = [];
if (mobile.isNotEmpty) { String pinValue = '';
_mobileController.text = mobile;
} // Error states
} bool _emailHasError = false;
} bool _pinHasError = false;
@override @override
void dispose() { void initState() {
_mobileController.dispose(); super.initState();
super.dispose(); // Initialize PIN controllers and focus nodes
} for (int i = 0; i < pinLength; i++) {
_pinControllers.add(TextEditingController());
Future<void> _handleLogin() async { _pinFocusNodes.add(FocusNode());
final mobile = _mobileController.text.trim(); }
if (!_validationPopup.validateMobileNumber(context, mobile)) { final args = Get.arguments;
return; if (args != null && args is Map<String, dynamic>) {
} final email = args['email'] ?? '';
if (email.isNotEmpty) {
await ref.read(loginProvider.notifier).login(mobile); _emailController.text = email;
final state = ref.read(loginProvider); }
}
state.when( }
data: (data) {
if (data['success'] == true) { @override
Get.toNamed(ConstRouters.otp, arguments: {'mobile': mobile}); void dispose() {
_validationPopup.showSuccessMessage( _emailController.dispose();
context, for (var controller in _pinControllers) {
"OTP sent successfully!", controller.dispose();
); }
} else if (data['error'] != null) { for (var node in _pinFocusNodes) {
_validationPopup.showErrorMessage(context, data['error'].toString()); node.dispose();
} }
}, super.dispose();
loading: () {}, }
error: (err, _) {
_validationPopup.showErrorMessage(context, "Error: $err"); void _onPinChange(int index, String value) {
}, if (_pinHasError) {
); setState(() => _pinHasError = false);
} }
if (value.isNotEmpty && index < pinLength - 1) {
@override _pinFocusNodes[index + 1].requestFocus();
Widget build(BuildContext context) { }
final loginState = ref.watch(loginProvider); if (value.isEmpty && index > 0) {
final policyAsync = ref.watch(policyProvider); _pinFocusNodes[index - 1].requestFocus();
final termsAsync = ref.watch(termsProvider); }
setState(() {
// Initialize responsive utils pinValue = _pinControllers.map((c) => c.text).join();
final r = ResponsiveUtils(context); });
}
// Responsive values
final logoWidth = r.getValue<double>( Future<void> _handleLogin() async {
mobile: 120, final email = _emailController.text.trim();
tablet: 141, final pin = pinValue.trim();
desktop: 160,
); setState(() {
final logoHeight = r.getValue<double>( _emailHasError = false;
mobile: 85, _pinHasError = false;
tablet: 100, });
desktop: 115,
); if (!_validationPopup.validateEmail(context, email)) {
final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); setState(() => _emailHasError = true);
final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); return;
final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12); }
final signupFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); if (!_validationPopup.validatePin(context, pin)) {
final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); setState(() => _pinHasError = true);
return;
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); await ref.read(loginProvider.notifier).loginWithPin(email, pin);
final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28); final state = ref.read(loginProvider);
return PopScope( state.when(
canPop: true, data: (data) {
onPopInvokedWithResult: (didPop, result) { if (data['success'] == true) {
// Handle custom behavior here if needed in the future _validationPopup.showSuccessMessage(
}, context,
child: Scaffold( "Login Successful!",
resizeToAvoidBottomInset: true, );
body: Container( Future.delayed(const Duration(seconds: 1), () {
width: double.infinity, Get.offAll(() => const MainController());
height: double.infinity, });
decoration: const BoxDecoration( } else if (data['error'] != null) {
gradient: LinearGradient( _validationPopup.showErrorMessage(context, data['error'].toString());
begin: Alignment.topLeft, setState(() {
end: Alignment.bottomRight, _emailHasError = true;
colors: [ _pinHasError = true;
Color(0xFFFFF8F0), });
Color(0xFFEBC894), }
Color(0xFFE8DAF2), },
Color(0xFFB49EF4), loading: () {},
], error: (err, _) {
stops: [0.0, 0.3, 0.6, 1.0], _validationPopup.showErrorMessage(context, "Error: $err");
), },
), );
child: SingleChildScrollView( }
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: r.screenHeight), @override
child: IntrinsicHeight( Widget build(BuildContext context) {
child: Center( final loginState = ref.watch(loginProvider);
child: CommonContainerAuth( final policyAsync = ref.watch(policyProvider);
child: Column( final termsAsync = ref.watch(termsProvider);
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, final r = ResponsiveUtils(context);
children: [
// Logo final logoWidth = r.getValue<double>(mobile: 120, tablet: 141, desktop: 160);
Image.asset( final logoHeight = r.getValue<double>(mobile: 85, tablet: 100, desktop: 115);
AppAssets.taxgildelogoauth, final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
width: logoWidth, final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
height: logoHeight, final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12);
), final signupFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
SizedBox(height: spacingSM), final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14);
// Title final pinBoxWidth = r.getValue<double>(mobile: 55, tablet: 60, desktop: 68);
Text( final pinBoxHeight = r.getValue<double>(mobile: 55, tablet: 60, desktop: 68);
"Login", final pinFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32);
style: AppTextStyles.semiBold.copyWith( final pinBoxMargin = r.getValue<double>(mobile: 8, tablet: 6, desktop: 8);
fontSize: titleFontSize, final pinBoxRadius = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
color: AppColors.authheading,
), final spacingXS = r.spacing(mobile: 10, tablet: 15, desktop: 18);
), final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24);
SizedBox(height: spacingSM), final spacingMD = r.spacing(mobile: 20, tablet: 22, desktop: 26);
final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28);
// Subtitle
Text( return PopScope(
"Enter your Mobile Number", canPop: true,
textAlign: TextAlign.center, child: Scaffold(
style: AppTextStyles.semiBold.copyWith( resizeToAvoidBottomInset: true,
fontSize: subtitleFontSize, body: Container(
color: AppColors.authleading, width: double.infinity,
), height: double.infinity,
), decoration: const BoxDecoration(
SizedBox(height: spacingLG), gradient: LinearGradient(
begin: Alignment.topLeft,
// Mobile TextField end: Alignment.bottomRight,
CommanTextFormField( colors: [
controller: _mobileController, Color(0xFFFFF8F0),
hintText: 'Enter your Mobile Number', Color(0xFFEBC894),
keyboardType: TextInputType.phone, Color(0xFFE8DAF2),
prefixIcon: Icons.mobile_screen_share, Color(0xFFB49EF4),
), ],
SizedBox(height: spacingLG), stops: [0.0, 0.3, 0.6, 1.0],
),
// Terms and Policy ),
Text.rich( child: SingleChildScrollView(
TextSpan( child: ConstrainedBox(
text: "By signing up, you agree to the ", constraints: BoxConstraints(minHeight: r.screenHeight),
style: AppTextStyles.medium.copyWith( child: IntrinsicHeight(
fontSize: termsFontSize, child: Center(
color: AppColors.authleading, child: CommonContainerAuth(
), child: Column(
children: [ mainAxisSize: MainAxisSize.min,
TextSpan( crossAxisAlignment: CrossAxisAlignment.center,
text: "Terms of Service ", children: [
style: AppTextStyles.bold.copyWith( Image.asset(
fontSize: termsFontSize, AppAssets.taxgildelogoauth,
color: AppColors.authtermsandcondition, width: logoWidth,
), height: logoHeight,
recognizer: TapGestureRecognizer() ),
..onTap = () { SizedBox(height: spacingSM),
termsAsync.when( Text(
data: (terms) { "Login",
CommonInfoPopup.show( style: AppTextStyles.semiBold.copyWith(
context: context, fontSize: titleFontSize,
title: color: AppColors.authheading,
terms.data?.title ?? ),
"Terms of Service", ),
content: SizedBox(height: spacingSM),
terms.data?.content ?? Text(
"No terms found.", "Enter your Email and PIN",
); textAlign: TextAlign.center,
}, style: AppTextStyles.semiBold.copyWith(
loading: () { fontSize: subtitleFontSize,
_validationPopup.showErrorMessage( color: AppColors.authleading,
context, ),
"Loading Terms...", ),
); SizedBox(height: spacingLG),
}, CommanTextFormField(
error: (err, _) { controller: _emailController,
_validationPopup.showErrorMessage( hintText: 'Enter your Email',
context, keyboardType: TextInputType.emailAddress,
"Failed to load Terms", prefixIcon: Icons.email_outlined,
); hasError: _emailHasError,
}, onChanged: (value) {
); if (_emailHasError) {
}, setState(() => _emailHasError = false);
), }
const TextSpan(text: "and "), },
TextSpan( ),
text: "Data Processing Agreement", SizedBox(height: spacingMD),
style: TextStyle(
fontWeight: FontWeight.w700, // PIN Boxes
fontSize: termsFontSize, Row(
color: AppColors.authtermsandcondition, mainAxisAlignment: MainAxisAlignment.center,
), children: List.generate(pinLength, (index) {
recognizer: TapGestureRecognizer() bool isFilled = _pinControllers[index].text.isNotEmpty;
..onTap = () { return Container(
policyAsync.when( margin: EdgeInsets.symmetric(horizontal: pinBoxMargin),
data: (policy) { width: pinBoxWidth,
CommonInfoPopup.show( height: pinBoxHeight,
context: context, decoration: BoxDecoration(
title: color: isFilled ? AppColors.commanbutton : Colors.white,
policy.data?.title ?? borderRadius: BorderRadius.circular(pinBoxRadius),
"Data Processing Agreement", border: Border.all(
content: color: _pinHasError
policy.data?.content ?? ? Colors.red
"No policy found.", : const Color(0xFFDFDFDF),
); width: _pinHasError ? 1.5 : 1.0,
}, ),
loading: () { boxShadow: [
_validationPopup.showErrorMessage( BoxShadow(
context, color: _pinHasError
"Loading Policy...", ? Colors.red.withOpacity(0.1)
); : const Color(0xFFBDBDBD).withOpacity(0.25),
}, blurRadius: 7,
error: (err, _) { offset: const Offset(0, 1),
_validationPopup.showErrorMessage( ),
context, ],
"Failed to load Policy", ),
); child: Center(
}, child: KeyboardListener(
); focusNode: FocusNode(),
}, onKeyEvent: (event) {
), if (event is KeyDownEvent &&
], event.logicalKey ==
), LogicalKeyboardKey.backspace) {
textAlign: TextAlign.center, if (_pinControllers[index]
), .text
.isEmpty &&
SizedBox(height: spacingSM), index > 0) {
Text("Or",style: AppTextStyles.medium.copyWith( _pinFocusNodes[index - 1]
fontSize: spacingSM, .requestFocus();
color: AppColors.authleading, }
),), }
SizedBox(height: spacingSM), },
child: TextField(
// Sign up controller: _pinControllers[index],
Text.rich( focusNode: _pinFocusNodes[index],
TextSpan( textAlign: TextAlign.center,
text: "Didn't Have account? ", keyboardType: TextInputType.number,
style: AppTextStyles.bold.copyWith( maxLength: 1,
fontSize: signupFontSize, obscureText: true,
color: AppColors.black, enabled: !loginState.isLoading,
), style: TextStyle(
children: [ fontFamily: "Gilroy",
TextSpan( fontWeight: FontWeight.w400,
text: "Sign Up", fontSize: pinFontSize,
style: AppTextStyles.bold.copyWith( color: isFilled
fontSize: signupFontSize, ? Colors.white
color: AppColors.authsignup, : Colors.black,
), ),
recognizer: TapGestureRecognizer() decoration: const InputDecoration(
..onTap = () { counterText: '',
Get.offNamed(ConstRouters.signup); border: InputBorder.none,
}, ),
), onChanged: (value) =>
], _onPinChange(index, value),
), ),
), ),
SizedBox(height: spacingLG), ),
);
// Login Button }),
loginState.isLoading ),
? const CircularProgressIndicator()
: CommanButton( SizedBox(height: spacingXS),
text: "Login", Align(
onPressed: _handleLogin, alignment: Alignment.centerRight,
), child: GestureDetector(
onTap: () {
SizedBox(height: spacingMD), Get.toNamed(
ConstRouters.forgotPassword,
Text( arguments: {'isEmployee': false},
"Other Login", );
textAlign: TextAlign.center, },
style: AppTextStyles.semiBold.copyWith( child: Text(
fontSize: otherLoginFontSize, "Forgot Password?",
height: 1.8, style: AppTextStyles.bold.copyWith(
letterSpacing: 0.13, fontSize: signupFontSize,
color: AppColors.authleading, color: AppColors.authchange,
), ),
), ),
),
SizedBox(height: spacingXS), ),
Text.rich( SizedBox(height: spacingLG),
TextSpan( Text.rich(
text: "Staff Login? ", TextSpan(
style: AppTextStyles.bold.copyWith( text: "By signing up, you agree to the ",
fontSize: signupFontSize, style: AppTextStyles.medium.copyWith(
color: AppColors.black, fontSize: termsFontSize,
), color: AppColors.authleading,
children: [ ),
TextSpan( children: [
text: "Click Here", TextSpan(
style: AppTextStyles.bold.copyWith( text: "Terms of Service ",
fontSize: signupFontSize, style: AppTextStyles.bold.copyWith(
color: AppColors.authsignup, fontSize: termsFontSize,
), color: AppColors.authtermsandcondition,
recognizer: TapGestureRecognizer() ),
..onTap = () { recognizer: TapGestureRecognizer()
Get.offNamed(ConstRouters.employeelogin); ..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);
},
),
],
),
),
],
),
),
),
),
),
),
),
),
);
}
}

View File

@ -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<MpinSetScreen> createState() => _MpinSetScreenState();
}
class _MpinSetScreenState extends ConsumerState<MpinSetScreen> {
final ValidationPopup _validationPopup = ValidationPopup();
final int pinLength = 4;
final List<TextEditingController> _controllers = [];
final List<FocusNode> _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<double>(mobile: 120, tablet: 141, desktop: 160);
final logoHeight = r.getValue<double>(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<double>(mobile: 57, tablet: 60, desktop: 68);
final boxHeight = r.getValue<double>(mobile: 57, tablet: 56, desktop: 64);
final pinFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32);
final boxMargin = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
final boxRadius = r.getValue<double>(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,
),
],
),
),
),
),
),
],
),
);
}
}

View File

@ -1,411 +1,345 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/consts/comman_container_auth.dart'; import 'package:taxglide/consts/comman_container_auth.dart';
import 'package:taxglide/consts/responsive_helper.dart'; import 'package:taxglide/consts/responsive_helper.dart';
import 'package:taxglide/consts/validation_popup.dart'; import 'package:taxglide/consts/validation_popup.dart';
import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/router/consts_routers.dart'; import 'package:taxglide/router/consts_routers.dart';
import 'package:flutter/services.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart'; import 'package:taxglide/consts/app_style.dart';
import 'package:taxglide/consts/app_style.dart';
class OtpScreen extends ConsumerStatefulWidget {
class OtpScreen extends ConsumerStatefulWidget { const OtpScreen({super.key});
const OtpScreen({super.key});
@override
@override ConsumerState<OtpScreen> createState() => _OtpScreenState();
ConsumerState<OtpScreen> createState() => _OtpScreenState(); }
}
class _OtpScreenState extends ConsumerState<OtpScreen> {
class _OtpScreenState extends ConsumerState<OtpScreen> { final ValidationPopup _validationPopup = ValidationPopup();
final ValidationPopup _validationPopup = ValidationPopup(); final int otpLength = 4;
final int otpLength = 4; final List<TextEditingController> _controllers = [];
final List<TextEditingController> _controllers = []; final List<FocusNode> _focusNodes = [];
final List<FocusNode> _focusNodes = []; String otpValue = '';
String otpValue = '';
Timer? _timer;
Timer? _timer; int _remainingSeconds = 600;
int _remainingSeconds = 600; bool _canResend = false;
bool _canResend = false;
late String _identifier; // Email or Mobile
late String mobile; bool _fromForgot = false;
bool _isEmployee = false;
@override
void initState() { @override
super.initState(); void initState() {
final args = Get.arguments as Map<String, dynamic>; super.initState();
mobile = args['mobile'] ?? ''; final args = Get.arguments as Map<String, dynamic>? ?? {};
_fromForgot = args['fromForgot'] ?? false;
for (int i = 0; i < otpLength; i++) { _isEmployee = args['isEmployee'] ?? false;
_controllers.add(TextEditingController()); _identifier = args['email'] ?? args['mobile'] ?? '';
_focusNodes.add(FocusNode());
} for (int i = 0; i < otpLength; i++) {
_startTimer(); _controllers.add(TextEditingController());
} _focusNodes.add(FocusNode());
}
@override _startTimer();
void dispose() { }
_timer?.cancel();
for (var c in _controllers) c.dispose(); @override
for (var f in _focusNodes) f.dispose(); void dispose() {
super.dispose(); _timer?.cancel();
} for (var c in _controllers) c.dispose();
for (var f in _focusNodes) f.dispose();
void _startTimer() { super.dispose();
_canResend = false; }
_remainingSeconds = 30;
_timer?.cancel(); void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { _canResend = false;
if (_remainingSeconds > 0) { _remainingSeconds = 30;
setState(() => _remainingSeconds--); _timer?.cancel();
} else { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() => _canResend = true); if (_remainingSeconds > 0) {
timer.cancel(); 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')}'; String _formatTime(int seconds) {
} int minutes = seconds ~/ 60;
int remainingSeconds = seconds % 60;
void _resendOtp() async { return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
if (!_canResend) return; }
for (var controller in _controllers) controller.clear();
setState(() => otpValue = ''); void _resendOtp() async {
_focusNodes[0].requestFocus(); if (!_canResend) return;
for (var controller in _controllers) controller.clear();
try { setState(() => otpValue = '');
await ref.read(loginProvider.notifier).login(mobile); _focusNodes[0].requestFocus();
final loginState = ref.read(loginProvider);
loginState.when( if (_fromForgot) {
data: (result) { await ref.read(forgotPasswordProvider.notifier).requestOtp(_identifier, _isEmployee);
if (result['success'] == true) { final state = ref.read(forgotPasswordProvider);
_validationPopup.showSuccessMessage( state.when(
context, data: (result) {
"OTP has been resent successfully!", if (result['success'] == true) {
); _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!");
_startTimer(); _startTimer();
for (var controller in _controllers) controller.clear(); } else {
setState(() => otpValue = ''); _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP");
} else { }
_validationPopup.showErrorMessage( },
context, loading: () {},
result['error'] ?? "Failed to resend OTP", error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"),
); );
} } else {
}, // Original mobile login resend (if still needed)
loading: () {}, await ref.read(loginProvider.notifier).login(_identifier);
error: (error, stack) { final loginState = ref.read(loginProvider);
_validationPopup.showErrorMessage( loginState.when(
context, data: (result) {
"Failed to resend OTP. Please try again.", if (result['success'] == true) {
); _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!");
}, _startTimer();
); } else {
} catch (e) { _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP");
_validationPopup.showErrorMessage( }
context, },
"An error occurred. Please try again.", loading: () {},
); error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"),
} );
} }
}
void _verifyOtp() async {
if (otpValue.length != otpLength) { void _verifyOtp() async {
_validationPopup.showErrorMessage(context, "Please enter all OTP digits"); if (otpValue.length != otpLength) {
return; _validationPopup.showErrorMessage(context, "Please enter all OTP digits");
} return;
}
await ref.read(loginProvider.notifier).verifyOtp(mobile, otpValue);
final loginState = ref.read(loginProvider); if (_fromForgot) {
await ref.read(forgotPasswordProvider.notifier).verifyOtp(_identifier, otpValue);
loginState.when( final state = ref.read(forgotPasswordProvider);
data: (result) { state.when(
if (result['success'] == true) { data: (result) {
_validationPopup.showSuccessMessage( if (result['success'] == true) {
context, _validationPopup.showSuccessMessage(context, "OTP Verified successfully!");
"OTP Verified Successfully!", Get.toNamed(ConstRouters.mpinSet);
); } else {
Future.delayed(const Duration(seconds: 1), () { _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP");
Get.offAll(() => MainController()); }
}); },
} else { loading: () {},
_validationPopup.showErrorMessage( error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"),
context, );
result['error'] ?? "Invalid OTP. Please try again.", } else {
); // Original mobile login verify
} await ref.read(loginProvider.notifier).verifyOtp(_identifier, otpValue);
}, final state = ref.read(loginProvider);
loading: () {}, state.when(
error: (error, stack) { data: (result) {
_validationPopup.showErrorMessage( if (result['success'] == true) {
context, _validationPopup.showSuccessMessage(context, "OTP Verified successfully!");
"Failed to verify OTP. Please try again.", Get.offAllNamed(ConstRouters.mpinSet);
); } else {
}, _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP");
); }
} },
loading: () {},
void _onOtpChange(int index, String value) { error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"),
if (value.isNotEmpty && index < otpLength - 1) { );
_focusNodes[index + 1].requestFocus(); }
} }
if (value.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus(); void _onOtpChange(int index, String value) {
} if (value.isNotEmpty && index < otpLength - 1) {
setState(() { _focusNodes[index + 1].requestFocus();
otpValue = _controllers.map((c) => c.text).join(); }
}); if (value.isEmpty && index > 0) {
} _focusNodes[index - 1].requestFocus();
}
@override setState(() {
Widget build(BuildContext context) { otpValue = _controllers.map((c) => c.text).join();
final loginState = ref.watch(loginProvider); });
final isLoading = loginState is AsyncLoading; }
// Initialize responsive utils @override
final r = ResponsiveUtils(context); Widget build(BuildContext context) {
final isLoading = _fromForgot
// Responsive values ? ref.watch(forgotPasswordProvider).isLoading
final logoWidth = r.getValue<double>( : ref.watch(loginProvider).isLoading;
mobile: 120,
tablet: 141, final r = ResponsiveUtils(context);
desktop: 160,
); final logoWidth = r.getValue<double>(mobile: 120, tablet: 141, desktop: 160);
final logoHeight = r.getValue<double>( final logoHeight = r.getValue<double>(mobile: 85, tablet: 100, desktop: 115);
mobile: 85, final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
tablet: 100, final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
desktop: 115, final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
); final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18);
final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36);
final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); final otpBoxWidth = r.getValue<double>(mobile: 52, tablet: 60, desktop: 68);
final mobileFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); final otpBoxHeight = r.getValue<double>(mobile: 48, tablet: 56, desktop: 64);
final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32);
final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); final otpBoxMargin = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
final otpBoxRadius = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8);
final otpBoxWidth = r.getValue<double>(mobile: 52, tablet: 60, desktop: 68);
final otpBoxHeight = r.getValue<double>( final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24);
mobile: 48, final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26);
tablet: 56, final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36);
desktop: 64,
); return Scaffold(
final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); body: Stack(
final otpBoxMargin = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8); children: [
final otpBoxRadius = r.getValue<double>(mobile: 5, tablet: 6, desktop: 8); Container(
decoration: const BoxDecoration(
final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); gradient: LinearGradient(
final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); begin: Alignment.topLeft,
final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); end: Alignment.bottomRight,
final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); colors: [
Color(0xFFFFF8F0),
return Scaffold( Color(0xFFEBC894),
body: Stack( Color(0xFFE8DAF2),
children: [ Color(0xFFB49EF4),
// Background gradient ],
Container( stops: [0.0, 0.3, 0.6, 1.0],
decoration: const BoxDecoration( ),
gradient: LinearGradient( ),
begin: Alignment.topLeft, ),
end: Alignment.bottomRight, SingleChildScrollView(
colors: [ child: SizedBox(
Color(0xFFFFF8F0), height: r.screenHeight,
Color(0xFFEBC894), child: Center(
Color(0xFFE8DAF2), child: CommonContainerAuth(
Color(0xFFB49EF4), child: Column(
], mainAxisSize: MainAxisSize.min,
stops: [0.0, 0.3, 0.6, 1.0], children: [
), Image.asset(
), AppAssets.taxgildelogoauth,
), width: logoWidth,
height: logoHeight,
// Scrollable content fit: BoxFit.contain,
SingleChildScrollView( ),
child: SizedBox( SizedBox(height: spacingSM),
height: r.screenHeight, Text(
child: Center( "Enter OTP",
child: CommonContainerAuth( style: AppTextStyles.bold.copyWith(
child: Column( fontSize: titleFontSize,
mainAxisSize: MainAxisSize.min, color: AppColors.authheading,
crossAxisAlignment: CrossAxisAlignment.center, ),
children: [ ),
// Logo SizedBox(height: spacingSM),
Image.asset( Text(
AppAssets.taxgildelogoauth, "OTP has been sent to ${_identifier}",
width: logoWidth, textAlign: TextAlign.center,
height: logoHeight, style: AppTextStyles.medium.copyWith(
fit: BoxFit.contain, fontSize: subtitleFontSize,
), color: AppColors.authleading,
SizedBox(height: spacingSM), ),
),
// Title SizedBox(height: spacingMD),
Text( Row(
"Enter OTP", mainAxisAlignment: MainAxisAlignment.center,
textAlign: TextAlign.center, children: List.generate(otpLength, (index) {
style: AppTextStyles.semiBold.copyWith( bool isFilled = _controllers[index].text.isNotEmpty;
fontSize: titleFontSize, return Container(
height: 1.3, margin: EdgeInsets.symmetric(horizontal: otpBoxMargin),
letterSpacing: 0.01 * titleFontSize, width: otpBoxWidth,
color: AppColors.authheading, height: otpBoxHeight,
), decoration: BoxDecoration(
), color: isFilled ? AppColors.commanbutton : Colors.white,
SizedBox(height: spacingSM), borderRadius: BorderRadius.circular(otpBoxRadius),
border: Border.all(color: const Color(0xFFDFDFDF)),
// Subtitle boxShadow: [
Text( BoxShadow(
"OTP has been sent to your registered mobile number", color: const Color(0xFFBDBDBD).withOpacity(0.25),
textAlign: TextAlign.center, blurRadius: 7,
style: AppTextStyles.semiBold.copyWith( offset: const Offset(0, 1),
fontSize: subtitleFontSize, ),
height: 1.4, ],
letterSpacing: 0.03 * subtitleFontSize, ),
color: AppColors.authleading, child: Center(
), child: KeyboardListener(
), focusNode: FocusNode(),
SizedBox(height: spacingMD), onKeyEvent: (event) {
if (event is KeyDownEvent &&
// Mobile number + Change event.logicalKey ==
Text.rich( LogicalKeyboardKey.backspace) {
TextSpan( if (_controllers[index].text.isEmpty &&
text: mobile, index > 0) {
style: AppTextStyles.medium.copyWith( _focusNodes[index - 1].requestFocus();
fontSize: mobileFontSize, }
height: 1.8, }
letterSpacing: 0.01, },
color: AppColors.authleading, child: TextField(
), controller: _controllers[index],
children: [ focusNode: _focusNodes[index],
TextSpan( textAlign: TextAlign.center,
text: " ( Change Number )", keyboardType: TextInputType.number,
style: AppTextStyles.extraBold.copyWith( maxLength: 1,
fontSize: mobileFontSize, enabled: !isLoading,
height: 1.8, style: TextStyle(
letterSpacing: 0.04, fontFamily: "Gilroy",
color: AppColors.authchange, fontWeight: FontWeight.w400,
), fontSize: otpFontSize,
recognizer: TapGestureRecognizer() color: isFilled ? Colors.white : Colors.black,
..onTap = () { ),
Get.offNamed( decoration: const InputDecoration(
ConstRouters.login, counterText: '',
arguments: {'mobile': mobile}, border: InputBorder.none,
); ),
}, onChanged: (value) =>
), _onOtpChange(index, value),
], ),
), ),
textAlign: TextAlign.center, ),
), );
SizedBox(height: spacingLG), }),
),
// OTP Boxes SizedBox(height: spacingSM),
Row( Align(
mainAxisAlignment: MainAxisAlignment.center, alignment: Alignment.centerRight,
children: List.generate(otpLength, (index) { child: GestureDetector(
bool isFilled = _controllers[index].text.isNotEmpty; onTap: _canResend && !isLoading ? _resendOtp : null,
return Container( child: Text(
margin: EdgeInsets.symmetric( "Resend",
horizontal: otpBoxMargin, style: AppTextStyles.medium.copyWith(
), fontSize: resendFontSize,
width: otpBoxWidth, color: _canResend && !isLoading
height: otpBoxHeight, ? AppColors.authchange
decoration: BoxDecoration( : Colors.grey,
color: isFilled ),
? AppColors.commanbutton ),
: Colors.white, ),
borderRadius: BorderRadius.circular(otpBoxRadius), ),
border: Border.all( SizedBox(height: spacingSM),
color: const Color(0xFFDFDFDF), Text(
), _formatTime(_remainingSeconds),
boxShadow: [ style: AppTextStyles.semiBold.copyWith(
BoxShadow( fontSize: timerFontSize,
color: const Color( color: _remainingSeconds > 0 ? AppColors.authheading : Colors.red,
0xFFBDBDBD, ),
).withOpacity(0.25), ),
blurRadius: 7, SizedBox(height: spacingLG),
offset: const Offset(0, 1), isLoading
), ? const CircularProgressIndicator()
], : CommanButton(text: "Verify", onPressed: _verifyOtp),
), ],
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),
],
),
),
),
),
),
],
),
);
}
}

View File

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import 'package:get/get.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_asstes.dart';
import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/comman_button.dart';
@ -155,7 +157,7 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
} }
// Call verify OTP API for signup // 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); final signupState = ref.read(signupProvider);
@ -169,7 +171,7 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
// Navigate to main screen after successful verification // Navigate to main screen after successful verification
Future.delayed(const Duration(seconds: 1), () { Future.delayed(const Duration(seconds: 1), () {
Get.offAll(() => MainController()); Get.offAll(() => MpinSetScreen());
}); });
} else { } else {
_validationPopup.showErrorMessage( _validationPopup.showErrorMessage(
@ -208,7 +210,20 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
final signupState = ref.watch(signupProvider); final signupState = ref.watch(signupProvider);
final isLoading = signupState is AsyncLoading; 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( body: Stack(
children: [ children: [
// Background gradient // Background gradient
@ -257,10 +272,10 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
"OTP has been sent to your registered mobile number", "OTP has been sent to your registered mail id",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: AppTextStyles.semiBold.copyWith( style: AppTextStyles.semiBold.copyWith(
fontSize: 14, fontSize: 12,
height: 1.4, height: 1.4,
letterSpacing: 0.03 * 14, letterSpacing: 0.03 * 14,
color: AppColors.authleading, color: AppColors.authleading,
@ -269,7 +284,7 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
const SizedBox(height: 22), const SizedBox(height: 22),
Text.rich( Text.rich(
TextSpan( TextSpan(
text: mobile, text: email,
style: AppTextStyles.medium.copyWith( style: AppTextStyles.medium.copyWith(
fontSize: 13, fontSize: 13,
height: 1.8, height: 1.8,
@ -278,7 +293,7 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
), ),
children: [ children: [
TextSpan( TextSpan(
text: " ( Change Number )", text: " ( Change mail id )",
style: AppTextStyles.extraBold.copyWith( style: AppTextStyles.extraBold.copyWith(
fontSize: 13, fontSize: 13,
height: 1.8, height: 1.8,
@ -331,26 +346,39 @@ class _RegisterOtpScreenState extends ConsumerState<RegisterOtpScreen> {
], ],
), ),
child: Center( child: Center(
child: TextField( child: KeyboardListener(
controller: _controllers[index], focusNode: FocusNode(),
focusNode: _focusNodes[index], onKeyEvent: (event) {
textAlign: TextAlign.center, if (event is KeyDownEvent &&
keyboardType: TextInputType.number, event.logicalKey ==
maxLength: 1, LogicalKeyboardKey.backspace) {
enabled: !isLoading, if (_controllers[index].text.isEmpty &&
style: TextStyle( index > 0) {
fontFamily: "Gilroy", _focusNodes[index - 1].requestFocus();
fontWeight: FontWeight.w400, }
fontSize: 28, }
letterSpacing: 0.03 * 28, },
color: isFilled ? Colors.white : Colors.black, 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<RegisterOtpScreen> {
), ),
], ],
), ),
)
); );
} }
} }

View File

@ -1,106 +1,127 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/app_colors.dart';
class ValidationPopup { class ValidationPopup {
// Show SnackBar // Show SnackBar
void _showSnackBar(BuildContext context, String msg, {bool isError = false}) { void _showSnackBar(BuildContext context, String msg, {bool isError = false}) {
print("Mageshwaran $msg"); print("Mageshwaran $msg");
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
backgroundColor: isError backgroundColor: isError
? Colors.red ? Colors.red
: AppColors.commanbutton, // 🔴 Error red : AppColors.commanbutton, // 🔴 Error red
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 6, elevation: 6,
margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25), margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
content: SizedBox( content: SizedBox(
width: 200, width: 200,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18), Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
msg, msg,
style: const TextStyle(color: Colors.white, fontSize: 12), style: const TextStyle(color: Colors.white, fontSize: 12),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
), ),
), ),
], ],
), ),
), ),
), ),
); );
} }
// Validate Mobile Number // Validate Mobile Number
bool validateMobileNumber(BuildContext context, String mobileNumber) { bool validateMobileNumber(BuildContext context, String mobileNumber) {
// Check if empty // Check if empty
if (mobileNumber.isEmpty) { if (mobileNumber.isEmpty) {
_showSnackBar(context, "Please enter mobile number", isError: true); _showSnackBar(context, "Please enter mobile number", isError: true);
return false; return false;
} }
// Remove spaces and special characters // Remove spaces and special characters
String cleanNumber = mobileNumber.replaceAll(RegExp(r'[^\d]'), ''); String cleanNumber = mobileNumber.replaceAll(RegExp(r'[^\d]'), '');
// Check if contains only digits // Check if contains only digits
if (!RegExp(r'^[0-9]+$').hasMatch(cleanNumber)) { if (!RegExp(r'^[0-9]+$').hasMatch(cleanNumber)) {
_showSnackBar( _showSnackBar(
context, context,
"Mobile number must contain only digits", "Mobile number must contain only digits",
isError: true, isError: true,
); );
return false; return false;
} }
// Check length (Indian mobile numbers are 10 digits) // Check length (Indian mobile numbers are 10 digits)
if (cleanNumber.length != 10) { if (cleanNumber.length != 10) {
_showSnackBar(context, "Mobile number must be 10 digits", isError: true); _showSnackBar(context, "Mobile number must be 10 digits", isError: true);
return false; return false;
} }
// Check if starts with valid digit (6-9 for Indian numbers) // Check if starts with valid digit (6-9 for Indian numbers)
if (!RegExp(r'^[6-9]').hasMatch(cleanNumber)) { if (!RegExp(r'^[6-9]').hasMatch(cleanNumber)) {
_showSnackBar( _showSnackBar(
context, context,
"Mobile number must start with 6, 7, 8, or 9", "Mobile number must start with 6, 7, 8, or 9",
isError: true, isError: true,
); );
return false; return false;
} }
// All validations passed // All validations passed
return true; return true;
} }
// Add this method to your ValidationPopup class // Validate Email
bool validateEmail(BuildContext context, String email) { bool validateEmail(BuildContext context, String email) {
// Email regex pattern if (email.isEmpty) {
final emailRegex = RegExp( showErrorMessage(context, "Please enter your email address");
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', return false;
); }
// Email regex pattern
if (!emailRegex.hasMatch(email)) { final emailRegex = RegExp(
showErrorMessage(context, "Please enter a valid email address"); r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
return false; );
}
return true; if (!emailRegex.hasMatch(email)) {
} showErrorMessage(context, "Please enter a valid email address");
return false;
// Show success message }
void showSuccessMessage(BuildContext context, String msg) { return true;
_showSnackBar(context, msg, isError: false); }
}
// Validate PIN
// Show error message bool validatePin(BuildContext context, String pin) {
void showErrorMessage(BuildContext context, String msg) { if (pin.isEmpty) {
_showSnackBar(context, msg, isError: true); 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);
}
}

View File

@ -2,7 +2,7 @@ class ConstsApi {
static const String baseUrl = "https://www.taxglide.amrithaa.net"; static const String baseUrl = "https://www.taxglide.amrithaa.net";
static const String login = "$baseUrl/api/otp/sms/request"; 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 signup = "$baseUrl/api/register";
static const String terms = "$baseUrl/api/get_terms_conditions"; static const String terms = "$baseUrl/api/get_terms_conditions";
static const String policy = "$baseUrl/api/get_privacy_policy"; 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 dashboard = "$baseUrl/api/get_dashboard_data";
static const String proformaaccept = "$baseUrl/api/proforma/accept"; static const String proformaaccept = "$baseUrl/api/proforma/accept";
static const String payNow = "$baseUrl/api/pay_now"; 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";
} }

View File

@ -33,6 +33,17 @@ class LoginNotifier extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
LoginNotifier(this._apiRepository) : super(const AsyncValue.data({})); LoginNotifier(this._apiRepository) : super(const AsyncValue.data({}));
Future<void> 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<void> login(String mobile) async { Future<void> login(String mobile) async {
if (!mounted) return; if (!mounted) return;
state = const AsyncValue.loading(); state = const AsyncValue.loading();
@ -86,11 +97,11 @@ class SignupNotifier extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
} }
// Verify OTP for signup // Verify OTP for signup
Future<void> verifySignupOtp(String mobile, String otp) async { Future<void> verifySignupOtp(String email, String otp) async {
if (!mounted) return; if (!mounted) return;
state = const AsyncValue.loading(); state = const AsyncValue.loading();
try { try {
final result = await _apiRepository.verifySignupOtp(mobile, otp); final result = await _apiRepository.verifySignupOtp(email, otp);
if (mounted) state = AsyncValue.data(result); if (mounted) state = AsyncValue.data(result);
} catch (e, st) { } catch (e, st) {
if (mounted) state = AsyncValue.error(e, st); if (mounted) state = AsyncValue.error(e, st);
@ -115,6 +126,17 @@ class EmployeeLoginNotifier
EmployeeLoginNotifier(this._apiRepository) : super(const AsyncValue.data({})); EmployeeLoginNotifier(this._apiRepository) : super(const AsyncValue.data({}));
Future<void> 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<void> login(String mobile) async { Future<void> login(String mobile) async {
if (!mounted) return; if (!mounted) return;
state = const AsyncValue.loading(); state = const AsyncValue.loading();
@ -137,6 +159,77 @@ class EmployeeLoginNotifier
if (mounted) state = AsyncValue.error(e, st); if (mounted) state = AsyncValue.error(e, st);
} }
} }
void reset() {
state = const AsyncValue.data({});
}
}
final forgotPasswordProvider =
StateNotifierProvider<
ForgotPasswordNotifier,
AsyncValue<Map<String, dynamic>>
>((ref) => ForgotPasswordNotifier(ref.read(apiRepositoryProvider)));
class ForgotPasswordNotifier
extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
final ApiRepository _apiRepository;
ForgotPasswordNotifier(this._apiRepository)
: super(const AsyncValue.data({}));
Future<void> 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<void> 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<PinNotifier, AsyncValue<Map<String, dynamic>>>(
(ref) => PinNotifier(ref.read(apiRepositoryProvider)),
);
class PinNotifier extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
final ApiRepository _apiRepository;
PinNotifier(this._apiRepository) : super(const AsyncValue.data({}));
Future<void> 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<TermsModel>((ref) async { final termsProvider = FutureProvider<TermsModel>((ref) async {

View File

@ -159,6 +159,54 @@ class ApiRepository {
} }
} }
// 🔹 DIRECT LOGIN (Email + PIN) - Regenerates FCM token
Future<Map<String, dynamic>> 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 // 🔹 VERIFY OTP (LOGIN) - Regenerates FCM token
Future<Map<String, dynamic>> verifyOtp(LoginModel model) async { Future<Map<String, dynamic>> verifyOtp(LoginModel model) async {
try { try {
@ -230,6 +278,143 @@ class ApiRepository {
} }
} }
// 🔹 EMPLOYEE LOGIN (Email + PIN) - Regenerates FCM token
Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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 // 🔹 SIGNUP API
Future<Map<String, dynamic>> signupUser(SignupModel model) async { Future<Map<String, dynamic>> signupUser(SignupModel model) async {
try { try {
@ -258,7 +443,7 @@ class ApiRepository {
// 🔹 VERIFY OTP (SIGNUP) - Regenerates FCM token // 🔹 VERIFY OTP (SIGNUP) - Regenerates FCM token
Future<Map<String, dynamic>> verifySignupOtp( Future<Map<String, dynamic>> verifySignupOtp(
String mobile, String email,
String otp, String otp,
) async { ) async {
try { try {
@ -267,7 +452,7 @@ class ApiRepository {
final fcmToken = tokens['fcm_token']; final fcmToken = tokens['fcm_token'];
final apnsToken = tokens['apns_token']; final apnsToken = tokens['apns_token'];
final params = {'mobile': mobile, 'otp': otp}; final params = {'email': email, 'otp': otp};
if (fcmToken != null && fcmToken.isNotEmpty) { if (fcmToken != null && fcmToken.isNotEmpty) {
params['fcm_token'] = fcmToken; params['fcm_token'] = fcmToken;
@ -945,6 +1130,41 @@ debugPrint('📦 KYC Response Body: ${response.body}');
throw {'error': 'Failed to accept proforma: ${e.toString()}'}; throw {'error': 'Failed to accept proforma: ${e.toString()}'};
} }
} }
// // 🔹 CHANGE PIN (Verify OTP)
// Future<Map<String, dynamic>> 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 // 🔹 LOGOUT - Deletes FCM token
Future<void> logout() async { Future<void> logout() async {

View File

@ -1,23 +1,38 @@
// login_model.dart // login_model.dart
class LoginModel { class LoginModel {
String? mobile; String? email;
String? otp; String? pin;
String? mobile;
LoginModel({this.mobile, this.otp}); String? otp;
// For sending mobile number to request OTP LoginModel({this.email, this.pin, this.mobile, this.otp});
Map<String, dynamic> toJsonMobile() {
return {'mobile': mobile}; // For sending mobile number to request OTP (Legacy)
} Map<String, dynamic> toJsonMobile() {
return {'mobile': mobile};
// For sending mobile + OTP for verification }
Map<String, dynamic> toJsonOtp() {
return {'mobile': mobile, 'otp': otp}; // For sending mobile + OTP for verification (Legacy)
} Map<String, dynamic> toJsonOtp() {
return {'mobile': mobile, 'otp': otp};
// Response from API }
factory LoginModel.fromJson(Map<String, dynamic> json) {
return LoginModel(mobile: json['mobile'], otp: json['otp']); // For Email + PIN Login
} Map<String, dynamic> toJsonEmailPin() {
} return {
'email': email,
'pin': pin,
};
}
// Response from API
factory LoginModel.fromJson(Map<String, dynamic> json) {
return LoginModel(
email: json['email'],
pin: json['pin'],
mobile: json['mobile'],
otp: json['otp'],
);
}
}

View File

@ -13,4 +13,6 @@ class ConstRouters {
static const String servicerequest = '/servicerequest'; static const String servicerequest = '/servicerequest';
static const String staff = '/staff'; static const String staff = '/staff';
static const String employeekycdetailslist = '/employeekycdetailslist'; static const String employeekycdetailslist = '/employeekycdetailslist';
static const String mpinSet = '/mpinSet';
static const String forgotPassword = '/forgotPassword';
} }

View File

@ -1,7 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:taxglide/auth/employee_login_screen.dart'; import 'package:taxglide/auth/employee_login_screen.dart';
import 'package:taxglide/auth/employee_otp_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/login_screen.dart';
import 'package:taxglide/auth/mpin_set_screen.dart';
import 'package:taxglide/auth/otp_screen.dart'; import 'package:taxglide/auth/otp_screen.dart';
import 'package:taxglide/auth/register_otp_screen.dart'; import 'package:taxglide/auth/register_otp_screen.dart';
import 'package:taxglide/auth/signup_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.policy, page: () => const PolicyScreen()),
GetPage(name: ConstRouters.staff, page: () => const StaffListScreen()), GetPage(name: ConstRouters.staff, page: () => const StaffListScreen()),
GetPage(name: ConstRouters.mpinSet, page: () => const MpinSetScreen()),
GetPage(
name: ConstRouters.forgotPassword,
page: () => const ForgotScreen(),
),
]; ];
} }

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "93.0.0" version: "85.0.0"
_flutterfire_internals: _flutterfire_internals:
dependency: transitive dependency: transitive
description: description:
@ -21,10 +21,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" version: "7.7.1"
animated_notch_bottom_bar: animated_notch_bottom_bar:
dependency: "direct main" dependency: "direct main"
description: description:
@ -101,10 +101,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -724,26 +724,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.16.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -1137,26 +1137,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.29.0" version: "1.26.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.6"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.15" version: "0.6.11"
timezone: timezone:
dependency: transitive dependency: transitive
description: description: