first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
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,
),
],
);
}
}