Files
doormile_milderapp/lib/views/onboardscreens/otp_page.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

262 lines
8.7 KiB
Dart

import 'dart:async';
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/helpers/constants/Font_constant.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/controllers/auth.dart';
import 'package:miler/views/onboardscreens/Creat_mpin.dart';
import 'package:miler/views/onboardscreens/auth_scaffold.dart';
import 'package:sms_autofill/sms_autofill.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// VERIFY — step 2 of 3.
///
/// ── What changed and why ──
///
/// **The number is now on screen.** The old copy said "Enter the 6-digit code
/// sent to your device", which is the one screen in the flow where the rider
/// most needs to check a fact — *which* number did it go to — and it did not
/// tell him. If he fat-fingered a digit on the previous screen, he waits out a
/// 60-second timer for a code that was never coming, with no way to tell. It now
/// echoes the number with a "Change" action beside it, which is what every app
/// that has thought about this does.
///
/// **Failure is visible.** A wrong or expired code silently cleared all six
/// boxes and dropped a floating `SnackBar` at the bottom of the screen —
/// underneath the keyboard, which is up by definition here. The rider saw six
/// boxes empty themselves for no stated reason. The boxes now turn red and the
/// reason is printed under them.
///
/// **The decorative tile is gone**, along with the 64px tinted SMS glyph inside
/// it: it pushed the code boxes a third of the way down the screen to illustrate
/// a word already in the heading.
/// ─────────────────────────────────────────────────────────────────────────
class OtpPage extends GetResponsiveView {
OtpPage({super.key});
@override
Widget builder() => const _OtpPageLayout();
}
class _OtpPageLayout extends StatefulWidget {
const _OtpPageLayout();
@override
State<_OtpPageLayout> createState() => _OtpPageLayoutState();
}
class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill {
final AuthController _auth = Get.put(AuthController());
final List<TextEditingController> _otpControllers = List.generate(
6,
(_) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
bool isVerifying = false;
String? _error;
int _secondsRemaining = 60;
Timer? _timer;
@override
void initState() {
super.initState();
_startTimer();
listenForCode();
WidgetsBinding.instance.addPostFrameCallback(
(_) => _focusNodes[0].requestFocus(),
);
}
void _startTimer() {
_secondsRemaining = 60;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) {
timer.cancel();
return;
}
if (_secondsRemaining > 0) {
setState(() => _secondsRemaining--);
} else {
timer.cancel();
}
});
}
bool get _isComplete => _otpControllers.every((c) => c.text.isNotEmpty);
String get _code => _otpControllers.map((c) => c.text).join();
Future<void> _autoVerify() async {
if (isVerifying || !_isComplete) return;
setState(() {
isVerifying = true;
_error = null;
});
final ok = await _auth.verifyOtp(_code);
if (!mounted) return;
setState(() => isVerifying = false);
if (ok) {
openScreen(context, CreateMpin());
} else {
// Clear and refocus as before, but say so — see the class note.
for (final c in _otpControllers) {
c.clear();
}
_focusNodes[0].requestFocus();
setState(() {
_error = _secondsRemaining == 0
? 'That code is incorrect or has expired. Request a new one.'
: 'That code is incorrect. Check the SMS and try again.';
});
}
}
@override
void codeUpdated() {
final received = code ?? '';
if (received.length >= 6) {
for (int i = 0; i < 6; i++) {
_otpControllers[i].text = received[i];
}
setState(() => _error = null);
_autoVerify();
}
}
void _onDigitChanged(String val, int index) {
if (val.isNotEmpty && index < 5) {
_focusNodes[index + 1].requestFocus();
}
if (val.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
}
// Typing again clears a previous rejection — the red boxes refer to the code
// he just replaced, not the one he is entering now.
setState(() {
if (_error != null) _error = null;
});
if (_isComplete) _autoVerify();
}
/// `+91 98765 43210`, grouped so a rider can actually proof-read it.
String get _maskedPhone {
final raw = (_auth.currentPhone ?? '').replaceAll(RegExp(r'\D'), '');
if (raw.length != 10) return 'your mobile number';
return '+91 ${raw.substring(0, 5)} ${raw.substring(5)}';
}
@override
void dispose() {
for (var c in _otpControllers) {
c.dispose();
}
for (var f in _focusNodes) {
f.dispose();
}
_timer?.cancel();
cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final canResend = _secondsRemaining == 0;
return AuthScaffold(
onBack: () => Get.back(),
// 1 of 2, not 2 of 3: the number screen no longer counts itself, and a
// counter whose first sighting is "step 2" is a counter the rider has
// already lost track of. See the note at the top of `Sign_in`.
step: 1,
totalSteps: 2,
// Compact, for the same reason as the two screens either side of it: the
// first box takes focus on the first frame. Included so the flow does not
// gain and lose its masthead between the number screen and the PIN one.
banner: const AuthBrandBanner(compact: true),
title: 'Enter the 6-digit code',
footer: AuthPrimaryButton(
label: 'Verify & continue',
loading: isVerifying,
onPressed: isVerifying || !_isComplete ? null : _autoVerify,
),
children: [
// ── Which number, and a way out ──
// The single most useful fact on this screen, and the old version left
// it off entirely.
Padding(
padding: EdgeInsets.only(bottom: 26.h),
child: Row(
children: [
Expanded(
child: Text.rich(
TextSpan(
style: AuthType.subtitle,
children: [
const TextSpan(text: 'Sent by SMS to '),
TextSpan(
text: _maskedPhone,
style: TextStyle(
fontWeight: FontWeight.w700,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
),
SizedBox(width: 8.w),
AuthTextAction(
label: 'Change',
onPressed: isVerifying ? null : () => Get.back(),
),
],
),
),
AuthCodeRow(
controllers: _otpControllers,
nodes: _focusNodes,
hasError: _error != null,
onChanged: _onDigitChanged,
),
AuthInlineError(message: _error),
SizedBox(height: 22.h),
// ── Resend ──
//
// The countdown used to be a disabled `TextButton` reading
// "Resend in 00:42", so the affordance and the timer were the same
// control and it looked broken for its first sixty seconds. The wait is
// now a plain sentence, and the button only exists once it can be used.
Center(
child: canResend
? AuthTextAction(
label: 'Send a new code',
icon: LucideIcons.rotateCw,
onPressed: () async {
setState(() => _error = null);
_startTimer();
await _auth.sendOtp();
},
)
: Text(
'You can request a new code in '
'0:${_secondsRemaining.toString().padLeft(2, '0')}',
style: AuthType.meta,
),
),
],
);
}
}