Files
2026-08-11 13:16:33 +05:30

199 lines
6.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/views/onboardscreens/otp_page.dart';
import 'package:miler/views/onboardscreens/auth_scaffold.dart';
import 'package:miler/widget/Bottom_page.dart';
import 'package:miler/controllers/auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// UNLOCK — the returning rider's front door.
///
/// Deliberately **not** given a step indicator. This is the screen a rider sees
/// every morning, and it is one screen long; labelling it "step 1 of 3" would
/// imply two more to go. The step rail belongs to the one-time sign-up flow.
///
/// ── What changed and why ──
///
/// **It says whose account it is.** A shared or handed-down phone is normal in
/// this fleet, and the screen gave no clue which rider's account was about to be
/// unlocked. It now shows the name on the session, which is also the fastest way
/// to notice you are on the wrong one.
///
/// **The boxes match the rest of the flow.** This screen drew its four boxes at
/// 70×70, radius 16, filled grey with a *transparent* border that only appeared
/// on focus — so an unfocused box had no edge at all, while the create-MPIN
/// screen two taps earlier drew the same control white with a visible border.
/// Both now use the shared [AuthCodeRow].
///
/// **"Access your account securely" is gone.** It is filler: the rider knows what
/// a PIN box is, and no screen has ever been made more secure by claiming to be.
/// ─────────────────────────────────────────────────────────────────────────
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;
bool _rejected = false;
String _riderName = '';
@override
void initState() {
super.initState();
_loadRider();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNodes[0].requestFocus();
});
}
Future<void> _loadRider() async {
try {
final prefs = await SharedPreferences.getInstance();
final name = (prefs.getString('user_name') ?? '').trim();
if (mounted && name.isNotEmpty) {
setState(() => _riderName = name.split(' ').first);
}
} catch (_) {
// Non-essential: the screen works without a name.
}
}
@override
void dispose() {
for (var c in _mpinControllers) {
c.dispose();
}
for (var f in _focusNodes) {
f.dispose();
}
super.dispose();
}
bool get _isComplete => _mpinControllers.every((c) => c.text.isNotEmpty);
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();
}
setState(() {
if (_rejected) _rejected = false;
});
if (_isComplete) {
FocusScope.of(context).unfocus();
_submitMpin();
}
}
/// Wired to [AuthController.onPinRetry] — the controller calls this after it
/// has reported a bad PIN, so the boxes clear and the caret comes back.
void _clearMpinAndFocus() {
for (final c in _mpinControllers) {
c.clear();
}
_focusNodes[0].requestFocus();
if (mounted) setState(() => _rejected = true);
}
Future<void> _submitMpin() async {
if (!mounted || isVerifying) return;
_auth.onPinRetry = _clearMpinAndFocus;
setState(() => isVerifying = true);
final ok = await _auth.verifyPinWithServer(
_mpinControllers.map((c) => c.text).join(),
);
if (!mounted) return;
setState(() => isVerifying = false);
if (ok) {
// Straight to the app. There used to be a "Go on duty" gate between the
// PIN and the tabs, which made every launch a two-step sign-in for a
// decision the rider can make — and change — from the switch in Home's
// app bar at any moment of the shift.
Get.offAll(() => const BottomPage());
}
}
@override
Widget build(BuildContext context) {
return AuthScaffold(
onBack: () => Get.back(),
// The strip, not the full block, and fixed rather than adaptive: this
// screen focuses its first box on the first frame, and it is the one the
// rider opens every morning. See [AuthBrandBanner].
//
// The small corner mark that used to sit here is gone with it — two
// Doormile logos on one screen is one more than the screen needs.
banner: const AuthBrandBanner(compact: true),
title: _riderName.isEmpty
? 'Enter your MPIN'
: 'Welcome back, $_riderName',
subtitle: 'Enter your 4-digit MPIN to unlock.',
footer: Column(
mainAxisSize: MainAxisSize.min,
children: [
AuthPrimaryButton(
label: 'Unlock',
icon: Icons.lock_open_rounded,
loading: isVerifying,
onPressed: isVerifying || !_isComplete ? null : _submitMpin,
),
SizedBox(height: 6.h),
AuthTextAction(
label: 'Forgot MPIN?',
onPressed: isVerifying
? null
: () async {
await _auth.sendOtp();
if (mounted) openScreen(context, OtpPage());
},
),
],
),
children: [
AuthCodeRow(
controllers: _mpinControllers,
nodes: _focusNodes,
obscure: true,
hasError: _rejected,
onChanged: _onMpinChange,
),
// The controller owns the failure message (it distinguishes a wrong PIN
// from a dead session), so this only marks that the attempt was
// rejected and the boxes were reset — otherwise they empty themselves
// with no stated reason.
AuthInlineError(
message: _rejected ? 'Incorrect MPIN. Try again.' : null,
),
],
);
}
}