344 lines
12 KiB
Dart
344 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
|
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
|
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
|
import 'package:nearle/views/onboardscreens/otp_page.dart';
|
|
import 'package:nearle/widget/Bottom_page.dart';
|
|
import 'package:nearle/controllers/auth.dart';
|
|
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
class Mpin extends GetResponsiveView {
|
|
Mpin({super.key});
|
|
|
|
@override
|
|
Widget builder() {
|
|
return const _MpinView();
|
|
}
|
|
}
|
|
|
|
class _MpinView extends StatefulWidget {
|
|
const _MpinView();
|
|
|
|
@override
|
|
State<_MpinView> createState() => _MpinViewState();
|
|
}
|
|
|
|
class _MpinViewState extends State<_MpinView> {
|
|
final AuthController _auth = Get.put(AuthController());
|
|
final List<TextEditingController> _mpinControllers = List.generate(
|
|
4,
|
|
(_) => TextEditingController(),
|
|
);
|
|
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
|
|
|
bool isVerifying = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_focusNodes[0].requestFocus();
|
|
_maybeShowMasterPinReminder();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (var c in _mpinControllers) {
|
|
c.dispose();
|
|
}
|
|
for (var f in _focusNodes) {
|
|
f.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
void _onMpinChange(String value, int index) {
|
|
if (value.isNotEmpty && index < 3) {
|
|
_focusNodes[index + 1].requestFocus();
|
|
} else if (value.isEmpty && index > 0) {
|
|
_focusNodes[index - 1].requestFocus();
|
|
}
|
|
|
|
final filled = _mpinControllers.every((c) => c.text.isNotEmpty);
|
|
if (filled) {
|
|
FocusScope.of(context).unfocus();
|
|
_submitMpin(); // auto-verify when 4 digits are filled
|
|
}
|
|
|
|
setState(() {});
|
|
}
|
|
|
|
String getMpin() => _mpinControllers.map((c) => c.text).join();
|
|
bool get isMpinFilled => _mpinControllers.every((c) => c.text.isNotEmpty);
|
|
|
|
void _clearMpinAndFocus() {
|
|
for (final c in _mpinControllers) {
|
|
c.clear();
|
|
}
|
|
if (_focusNodes.isNotEmpty) {
|
|
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
Future<void> _submitMpin() async {
|
|
if (!mounted || isVerifying || !isMpinFilled) return;
|
|
|
|
// Register retry callback so AuthController bottom-sheet "Retry" button
|
|
// can clear MPIN boxes and bring back keyboard when PIN is wrong.
|
|
_auth.onPinRetry = _clearMpinAndFocus;
|
|
|
|
setState(() => isVerifying = true);
|
|
final mpin = getMpin();
|
|
final ok = await _auth.verifyPinWithServer(mpin);
|
|
|
|
if (!mounted) return;
|
|
setState(() => isVerifying = false);
|
|
|
|
if (ok) {
|
|
// ✅ Wait a moment to ensure onduty is saved, then read it
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final onduty = prefs.getInt('onduty') ?? 0;
|
|
|
|
debugPrint('[MPIN] After verification - onduty=$onduty');
|
|
|
|
// Navigate based on onduty value
|
|
if (onduty == 0) {
|
|
debugPrint('[MPIN] Navigating to Introscreen (onduty=0)');
|
|
Get.offAll(() => SigninBanner());
|
|
} else {
|
|
debugPrint('[MPIN] Navigating to BottomPage (onduty=1)');
|
|
Get.offAll(() => const BottomPage());
|
|
}
|
|
} else {
|
|
// Wrong MPIN: show alert/snackbar and keep user on MPIN screen
|
|
}
|
|
}
|
|
|
|
Future<void> _maybeShowMasterPinReminder() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final forceMasterPin =
|
|
prefs.getBool(AuthController.forceMasterPinPrefKey) ?? false;
|
|
if (forceMasterPin) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Use ${AuthController.masterPinValue} as PIN to continue.',
|
|
),
|
|
duration: const Duration(seconds: 4),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = MediaQuery.of(context).size;
|
|
final height = size.height;
|
|
final width = size.width;
|
|
|
|
double scale = 1.0;
|
|
if (Get.width < 380) scale = 0.9;
|
|
if (Get.width > 800) scale = 1.2;
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: Stack(
|
|
children: [
|
|
SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SizedBox(height: height * 0.04),
|
|
|
|
SizedBox(
|
|
height: height * 0.25 * scale,
|
|
width: width * 0.6,
|
|
child: Image.asset(
|
|
"assets/images/Mpin.png",
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: height * 0.04),
|
|
|
|
Text(
|
|
"Enter Your MPIN",
|
|
style: TextStyle(
|
|
fontSize: height * 0.05 * scale,
|
|
fontWeight: FontWeight.bold,
|
|
color: ColorConstants.primaryColor,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: height * 0.01),
|
|
|
|
Text(
|
|
"Access your account securely",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: height * 0.024 * scale,
|
|
color: Colors.black54,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: height * 0.06),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: List.generate(4, (index) {
|
|
final isFilled = _mpinControllers[index].text.isNotEmpty;
|
|
return SizedBox(
|
|
width: 50 * scale,
|
|
height: 55 * scale,
|
|
|
|
child: RawKeyboardListener(
|
|
focusNode: FocusNode(),
|
|
|
|
onKey: (event) {
|
|
if (event is RawKeyDownEvent &&
|
|
event.logicalKey ==
|
|
LogicalKeyboardKey.backspace &&
|
|
_mpinControllers[index].text.isEmpty &&
|
|
index > 0) {
|
|
_mpinControllers[index - 1].clear();
|
|
_focusNodes[index - 1].requestFocus();
|
|
setState(() {});
|
|
}
|
|
},
|
|
|
|
child: TextField(
|
|
controller: _mpinControllers[index],
|
|
focusNode: _focusNodes[index],
|
|
textAlign: TextAlign.center,
|
|
textAlignVertical: TextAlignVertical.center,
|
|
obscureText: true,
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 1,
|
|
style: TextStyle(
|
|
fontSize: height * 0.028 * scale,
|
|
fontWeight: FontWeight.bold,
|
|
color: isFilled ? Colors.white : Colors.black,
|
|
),
|
|
decoration: InputDecoration(
|
|
counterText: "",
|
|
contentPadding: EdgeInsets.zero,
|
|
filled: true,
|
|
fillColor: isFilled
|
|
? ColorConstants.primaryColor
|
|
: Colors.transparent,
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8 * scale),
|
|
borderSide: const BorderSide(
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8 * scale),
|
|
borderSide: const BorderSide(
|
|
color: Color(0xFF662582),
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
onChanged: (value) => _onMpinChange(value, index),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
|
|
SizedBox(height: height * 0.02),
|
|
|
|
// Retry + Forget Pin row
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () {
|
|
// Clear all MPIN boxes and focus first, bringing up keyboard
|
|
for (final c in _mpinControllers) {
|
|
c.clear();
|
|
}
|
|
if (_focusNodes.isNotEmpty) {
|
|
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
|
}
|
|
setState(() {});
|
|
},
|
|
child: Text(
|
|
"",
|
|
style: TextStyle(
|
|
fontSize: height * 0.022 * scale,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () async {
|
|
await _auth.sendOtp();
|
|
Get.to(OtpPage());
|
|
},
|
|
child: Transform.translate(
|
|
offset: Offset(-18, 0),
|
|
child: Text(
|
|
"Forget Pin?",
|
|
style: TextStyle(
|
|
fontSize: height * 0.025 * scale,
|
|
fontWeight: FontWeight.bold,
|
|
color: ColorConstants.primaryColor,
|
|
fontFamily: FontConstants.fontFamily,
|
|
decoration: TextDecoration.underline,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
top: height * 0.05,
|
|
left: width * 0.04,
|
|
child: InkWell(
|
|
onTap: () {
|
|
Get.to(SignIn());
|
|
},
|
|
borderRadius: BorderRadius.circular(30),
|
|
child: Container(
|
|
padding: EdgeInsets.all(width * 0.02),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.black12,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.arrow_back,
|
|
color: Colors.black,
|
|
size: width * 0.06,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|