import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.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/helpers/rider_shell.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 _mpinControllers = List.generate( 4, (_) => TextEditingController(), ); final List _focusNodes = List.generate(4, (_) => FocusNode()); bool isVerifying = false; bool _rejected = false; /// What actually went wrong, from [AuthController.lastPinFailure]. /// /// This screen used to hard-code "Incorrect MPIN. Try again." for every /// failure the controller reported — including the ones that never reached /// the server. A rider whose sign-in was dying on a device-id lookup was told /// his PIN was wrong, so the one thing he could report was the one thing that /// was not true. Whatever the controller knows, the screen now says. String? _failure; String _riderName = ''; @override void initState() { super.initState(); _loadRider(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNodes[0].requestFocus(); }); } Future _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; _failure = null; } }); 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; _failure = _auth.lastPinFailure; }); } } Future _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; // Say it on the screen, not only inside a sheet the rider dismisses. if (!ok) { _rejected = true; _failure = _auth.lastPinFailure; } }); 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. // // Which app depends on the tenant the login just resolved: `loginParsed` // persists `tenantid`/`tenantname` and calls `TenantController.load()` // before returning, so by this line the answer is settled. A delivery // rider lands in the ported Xpress flow, everyone else in the parcel // dashboard. See [riderShell]. Get.offAll(() => riderShell()); } } @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: LucideIcons.lockOpen, 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 ? (_failure ?? 'Incorrect MPIN. Try again.') : null, ), ], ); } }