first commit
This commit is contained in:
260
lib/views/onboardscreens/Creat_mpin.dart
Normal file
260
lib/views/onboardscreens/Creat_mpin.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:miler/views/helpers/widgets/page_transitions.dart';
|
||||
import 'package:miler/controllers/auth.dart';
|
||||
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/onboardscreens/Mpin.dart';
|
||||
import 'package:miler/views/onboardscreens/auth_scaffold.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// CREATE MPIN — step 3 of 3.
|
||||
///
|
||||
/// ── What changed and why ──
|
||||
///
|
||||
/// **A mismatch now says so.** Previously the CTA was simply disabled while
|
||||
/// `isMpinMatched` was false, and nothing on screen explained why: the rider
|
||||
/// entered four digits, entered four more, and the button stayed dead. Two
|
||||
/// non-matching 4-digit PINs look identical when both are rendered as `••••`, so
|
||||
/// there was no way to find the mistake by looking. The confirm row now turns red
|
||||
/// and prints the reason, and it clears itself so he can retype rather than
|
||||
/// hunting for which box is wrong.
|
||||
///
|
||||
/// **Weak PINs are refused.** `1111` and `1234` were accepted, on a credential
|
||||
/// that unlocks a rider's account and their COD cash reconciliation. Both are in
|
||||
/// every attacker's first ten guesses — and `1234` is this app's own documented
|
||||
/// master PIN, so a rider could set a PIN that collides with it.
|
||||
///
|
||||
/// **The decorative shield tile is gone**, and the two 4-digit rows are drawn by
|
||||
/// the shared [AuthCodeRow] instead of a private copy — this screen and the
|
||||
/// unlock screen were rendering the same control two different ways.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
class CreateMpin extends GetResponsiveView {
|
||||
CreateMpin({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
return const _CreateMpinBody();
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateMpinBody extends StatefulWidget {
|
||||
const _CreateMpinBody();
|
||||
|
||||
@override
|
||||
State<_CreateMpinBody> createState() => _CreateMpinBodyState();
|
||||
}
|
||||
|
||||
class _CreateMpinBodyState extends State<_CreateMpinBody> {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
|
||||
final List<TextEditingController> _newMpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<TextEditingController> _confirmMpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
|
||||
final List<FocusNode> _newFocusNodes = List.generate(4, (_) => FocusNode());
|
||||
final List<FocusNode> _confirmFocusNodes = List.generate(
|
||||
4,
|
||||
(_) => FocusNode(),
|
||||
);
|
||||
|
||||
bool isLoading = false;
|
||||
String? _error;
|
||||
|
||||
/// PINs that are trivially guessable. `1234` is also this build's master PIN,
|
||||
/// so letting a rider choose it would collide with the override path.
|
||||
static const _tooWeak = <String>{
|
||||
'1234',
|
||||
'4321',
|
||||
'0000',
|
||||
'1111',
|
||||
'2222',
|
||||
'3333',
|
||||
'4444',
|
||||
'5555',
|
||||
'6666',
|
||||
'7777',
|
||||
'8888',
|
||||
'9999',
|
||||
'1122',
|
||||
'2580',
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _newFocusNodes[0].requestFocus(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var c in [..._newMpinControllers, ..._confirmMpinControllers]) {
|
||||
c.dispose();
|
||||
}
|
||||
for (var f in [..._newFocusNodes, ..._confirmFocusNodes]) {
|
||||
f.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _pin(List<TextEditingController> c) => c.map((e) => e.text).join();
|
||||
|
||||
bool get _newFilled => _newMpinControllers.every((c) => c.text.isNotEmpty);
|
||||
bool get _confirmFilled =>
|
||||
_confirmMpinControllers.every((c) => c.text.isNotEmpty);
|
||||
bool get isMpinMatched =>
|
||||
_pin(_newMpinControllers) == _pin(_confirmMpinControllers);
|
||||
bool get isAllFilled => _newFilled && _confirmFilled;
|
||||
|
||||
void _onChanged(
|
||||
String value,
|
||||
int index,
|
||||
List<TextEditingController> controllers,
|
||||
List<FocusNode> nodes,
|
||||
) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
nodes[index + 1].requestFocus();
|
||||
} else if (value.isEmpty && index > 0) {
|
||||
nodes[index - 1].requestFocus();
|
||||
}
|
||||
|
||||
final isGroupFilled = controllers.every((c) => c.text.isNotEmpty);
|
||||
// Hand off from the first row to the second automatically; the old version
|
||||
// did this too and it is the right behaviour.
|
||||
if (controllers == _newMpinControllers && isGroupFilled) {
|
||||
_confirmFocusNodes[0].requestFocus();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_error = null;
|
||||
// Judge only once both rows are complete — grading a half-typed
|
||||
// confirmation would flash a mismatch on every single keystroke.
|
||||
if (controllers == _confirmMpinControllers && isGroupFilled) {
|
||||
FocusScope.of(context).unfocus();
|
||||
_error = _validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Null when the pair is acceptable.
|
||||
String? _validate() {
|
||||
final pin = _pin(_newMpinControllers);
|
||||
if (!isMpinMatched)
|
||||
return 'Those PINs do not match. Re-enter the second one.';
|
||||
if (_tooWeak.contains(pin)) {
|
||||
return 'That PIN is too easy to guess. Avoid runs and repeats.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _clearConfirm() {
|
||||
for (final c in _confirmMpinControllers) {
|
||||
c.clear();
|
||||
}
|
||||
_confirmFocusNodes[0].requestFocus();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final problem = _validate();
|
||||
if (problem != null) {
|
||||
setState(() => _error = problem);
|
||||
// A mismatch is almost always a typo in the confirmation, so clear that
|
||||
// row and put the caret in it. Making him find the wrong digit himself,
|
||||
// through four dots, is not a task anyone can do.
|
||||
if (!isMpinMatched) _clearConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
final ok = await _auth.setPin(_pin(_newMpinControllers));
|
||||
if (!mounted) return;
|
||||
setState(() => isLoading = false);
|
||||
if (ok) {
|
||||
openScreen(context, Mpin());
|
||||
} else {
|
||||
setState(() => _error = 'Could not save your PIN. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mismatch = _error != null && isAllFilled;
|
||||
|
||||
return AuthScaffold(
|
||||
onBack: () => Get.back(),
|
||||
// The last of the two counted screens — see the note in `otp_page`.
|
||||
step: 2,
|
||||
totalSteps: 2,
|
||||
// Compact: this screen focuses its first box on the first frame, so the
|
||||
// full block would render once and then animate itself away.
|
||||
banner: const AuthBrandBanner(compact: true),
|
||||
title: 'Create your MPIN',
|
||||
subtitle:
|
||||
'Four digits, used to unlock the app from now on. Do not share it — '
|
||||
'it also signs off the cash you collect.',
|
||||
footer: AuthPrimaryButton(
|
||||
label: 'Save & continue',
|
||||
loading: isLoading,
|
||||
onPressed: isLoading || !isAllFilled ? null : _save,
|
||||
),
|
||||
children: [
|
||||
Text('NEW MPIN', style: AuthType.fieldLabel),
|
||||
SizedBox(height: 10.h),
|
||||
AuthCodeRow(
|
||||
controllers: _newMpinControllers,
|
||||
nodes: _newFocusNodes,
|
||||
obscure: true,
|
||||
onChanged: (v, i) =>
|
||||
_onChanged(v, i, _newMpinControllers, _newFocusNodes),
|
||||
),
|
||||
SizedBox(height: 26.h),
|
||||
Row(
|
||||
children: [
|
||||
Text('CONFIRM MPIN', style: AuthType.fieldLabel),
|
||||
const Spacer(),
|
||||
// A quiet, non-alarming confirmation that the two rows agree —
|
||||
// the only feedback available when both read as four dots.
|
||||
if (isAllFilled && _error == null)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 14.sp,
|
||||
color: ColorConstants.acceptGreen,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Text(
|
||||
'Matches',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: ColorConstants.acceptGreen,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
AuthCodeRow(
|
||||
controllers: _confirmMpinControllers,
|
||||
nodes: _confirmFocusNodes,
|
||||
obscure: true,
|
||||
hasError: mismatch,
|
||||
onChanged: (v, i) =>
|
||||
_onChanged(v, i, _confirmMpinControllers, _confirmFocusNodes),
|
||||
),
|
||||
AuthInlineError(message: _error),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user