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>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

View File

@@ -1,4 +1,5 @@
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';
@@ -226,7 +227,7 @@ class _CreateMpinBodyState extends State<_CreateMpinBody> {
Row(
children: [
Icon(
Icons.check_circle_rounded,
LucideIcons.circleCheck,
size: 14.sp,
color: ColorConstants.acceptGreen,
),
@@ -235,7 +236,7 @@ class _CreateMpinBodyState extends State<_CreateMpinBody> {
'Matches',
style: TextStyle(
fontSize: 11.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: ColorConstants.acceptGreen,
fontFamily: FontConstants.fontFamily,
),

View File

@@ -1,10 +1,11 @@
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/widget/Bottom_page.dart';
import 'package:miler/helpers/rider_shell.dart';
import 'package:miler/controllers/auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -57,6 +58,15 @@ class _MpinViewState extends State<_MpinView> {
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
@@ -101,7 +111,10 @@ class _MpinViewState extends State<_MpinView> {
}
setState(() {
if (_rejected) _rejected = false;
if (_rejected) {
_rejected = false;
_failure = null;
}
});
if (_isComplete) {
@@ -117,7 +130,12 @@ class _MpinViewState extends State<_MpinView> {
c.clear();
}
_focusNodes[0].requestFocus();
if (mounted) setState(() => _rejected = true);
if (mounted) {
setState(() {
_rejected = true;
_failure = _auth.lastPinFailure;
});
}
}
Future<void> _submitMpin() async {
@@ -130,14 +148,27 @@ class _MpinViewState extends State<_MpinView> {
);
if (!mounted) return;
setState(() => isVerifying = false);
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.
Get.offAll(() => const BottomPage());
//
// 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());
}
}
@@ -161,7 +192,7 @@ class _MpinViewState extends State<_MpinView> {
children: [
AuthPrimaryButton(
label: 'Unlock',
icon: Icons.lock_open_rounded,
icon: LucideIcons.lockOpen,
loading: isVerifying,
onPressed: isVerifying || !_isComplete ? null : _submitMpin,
),
@@ -190,7 +221,9 @@ class _MpinViewState extends State<_MpinView> {
// rejected and the boxes were reset — otherwise they empty themselves
// with no stated reason.
AuthInlineError(
message: _rejected ? 'Incorrect MPIN. Try again.' : null,
message: _rejected
? (_failure ?? 'Incorrect MPIN. Try again.')
: null,
),
],
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
@@ -239,7 +240,7 @@ class _SignInState extends State<SignIn> {
cursorColor: ColorConstants.primary,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
@@ -258,7 +259,7 @@ class _SignInState extends State<SignIn> {
floatingLabelBehavior: FloatingLabelBehavior.always,
labelStyle: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
letterSpacing: -0.1,
color: error
? ColorConstants.errorRed
@@ -271,7 +272,7 @@ class _SignInState extends State<SignIn> {
hintStyle: TextStyle(
color: ColorConstants.secondaryText,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
letterSpacing: 1.2,
fontFamily: FontConstants.fontFamily,
),
@@ -292,7 +293,7 @@ class _SignInState extends State<SignIn> {
'+91',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
@@ -312,7 +313,7 @@ class _SignInState extends State<SignIn> {
? Padding(
padding: EdgeInsets.only(right: 14.w),
child: Icon(
Icons.check_circle_rounded,
LucideIcons.circleCheck,
size: 20.sp,
color: ColorConstants.acceptGreen,
),
@@ -357,7 +358,7 @@ class _SignInState extends State<SignIn> {
text: 'Terms of Service',
style: TextStyle(
color: ColorConstants.slateText,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
decoration: TextDecoration.underline,
decorationColor: ColorConstants.borderStrong,
fontFamily: FontConstants.fontFamily,
@@ -369,7 +370,7 @@ class _SignInState extends State<SignIn> {
text: 'Privacy Policy',
style: TextStyle(
color: ColorConstants.slateText,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
decoration: TextDecoration.underline,
decorationColor: ColorConstants.borderStrong,
fontFamily: FontConstants.fontFamily,
@@ -418,7 +419,9 @@ class _IndiaFlag extends StatelessWidget {
borderRadius: BorderRadius.circular(2.5.r),
child: Column(
children: [
const Expanded(child: ColoredBox(color: _saffron, child: SizedBox.expand())),
const Expanded(
child: ColoredBox(color: _saffron, child: SizedBox.expand()),
),
Expanded(
child: Container(
color: Colors.white,
@@ -433,7 +436,9 @@ class _IndiaFlag extends StatelessWidget {
),
),
),
const Expanded(child: ColoredBox(color: _green, child: SizedBox.expand())),
const Expanded(
child: ColoredBox(color: _green, child: SizedBox.expand()),
),
],
),
),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -50,7 +51,7 @@ import 'package:miler/views/helpers/widgets/app_widgets.dart';
abstract final class AuthType {
static TextStyle get title => TextStyle(
fontSize: 27.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.7,
height: 1.15,
color: ColorConstants.slateText,
@@ -67,7 +68,7 @@ abstract final class AuthType {
static TextStyle get fieldLabel => TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -75,7 +76,7 @@ abstract final class AuthType {
static TextStyle get meta => TextStyle(
fontSize: 12.5.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
height: 1.45,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -168,7 +169,7 @@ class AuthSteps extends StatelessWidget {
'STEP $step OF $total',
style: TextStyle(
fontSize: 10.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -188,7 +189,9 @@ class AuthSteps extends StatelessWidget {
color: i <= step
? ColorConstants.primary
: ColorConstants.borderStrong,
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
borderRadius: BorderRadius.circular(
DesignConstants.radiusFull,
),
),
),
),
@@ -278,7 +281,7 @@ class AuthScaffold extends StatelessWidget {
onTap: onBack!,
semanticLabel: 'Back',
child: Icon(
Icons.arrow_back_rounded,
LucideIcons.arrowLeft,
size: 22.sp,
color: ColorConstants.slateText,
),
@@ -385,7 +388,7 @@ class AuthScaffold extends StatelessWidget {
onTap: onBack!,
semanticLabel: 'Back',
child: Icon(
Icons.arrow_back_rounded,
LucideIcons.arrowLeft,
size: 22.sp,
color: ColorConstants.onAccent,
),
@@ -513,7 +516,8 @@ class AuthBrandBanner extends StatelessWidget {
duration: DesignConstants.motionState,
curve: Curves.easeOutCubic,
width: double.infinity,
height: (typing ? _typingHeight : _restHeight).h +
height:
(typing ? _typingHeight : _restHeight).h +
MediaQuery.paddingOf(context).top,
color: ColorConstants.primary,
child: ClipRect(
@@ -526,8 +530,16 @@ class AuthBrandBanner extends StatelessWidget {
// ornament. Bled off both edges and held at 7% white: at that
// weight they read as texture on the brand colour and never as
// objects the rider might try to tap.
Positioned(left: -78.w, top: -34.h, child: const _BrandRing(size: 196)),
Positioned(right: -92.w, bottom: -78.h, child: const _BrandRing(size: 244)),
Positioned(
left: -78.w,
top: -34.h,
child: const _BrandRing(size: 196),
),
Positioned(
right: -92.w,
bottom: -78.h,
child: const _BrandRing(size: 244),
),
Center(
child: Padding(
@@ -566,7 +578,7 @@ class AuthBrandBanner extends StatelessWidget {
'MILER · RIDER APP',
style: TextStyle(
fontSize: 10.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 1.6,
color: ColorConstants.onAccent.withValues(alpha: 0.72),
fontFamily: FontConstants.fontFamily,
@@ -667,7 +679,7 @@ class AuthPrimaryButton extends StatelessWidget {
const AuthPrimaryButton({
super.key,
required this.label,
this.icon = Icons.arrow_forward_rounded,
this.icon = LucideIcons.arrowRight,
this.loading = false,
required this.onPressed,
});
@@ -719,7 +731,7 @@ class AuthPrimaryButton extends StatelessWidget {
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
fontFamily: FontConstants.fontFamily,
),
@@ -774,7 +786,7 @@ class AuthTextAction extends StatelessWidget {
label,
style: TextStyle(
fontSize: 13.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: color,
fontFamily: FontConstants.fontFamily,
),
@@ -812,7 +824,7 @@ class AuthInlineError extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.error_outline_rounded,
LucideIcons.circleAlert,
size: 15.sp,
color: ColorConstants.errorRed,
),
@@ -823,7 +835,7 @@ class AuthInlineError extends StatelessWidget {
style: TextStyle(
color: ColorConstants.errorRed,
fontSize: 12.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
height: 1.35,
fontFamily: FontConstants.fontFamily,
),
@@ -865,31 +877,65 @@ class AuthCodeRow extends StatelessWidget {
this.hasError = false,
});
/// ── The boxes stopped stretching ──
///
/// Each box was `Expanded` inside an `AspectRatio`, so four of them divided
/// the *whole* content width between themselves: on a 390pt phone that is a
/// **79×83pt** box, four of them, filling the screen edge to edge. A PIN
/// field that size reads as a keypad rather than as an input — the digits
/// float in the middle of a large empty square, the group dominates a screen
/// whose only job is four taps, and it gets worse on a tablet, where the
/// boxes keep growing with the window.
///
/// Every app that does this well — the ones a rider already uses to bank and
/// to sign in — sizes the box to the *digit*, not to the screen, and centres
/// the group. 56pt is a comfortable target well above the 44pt floor, and
/// four of them plus their gaps come to 260 of the 390 available, so the
/// group reads as one control with air around it instead of a wall.
///
/// The size is a *preference*, not a constant: [LayoutBuilder] caps it at
/// whatever the width actually allows, so a 6-digit code on a 320pt phone
/// shrinks to fit rather than overflowing. It can get smaller than preferred;
/// it can never get bigger.
@override
Widget build(BuildContext context) {
final count = controllers.length;
// Taller boxes when there are fewer of them: a 4-digit PIN gets room to
// feel deliberate, a 6-digit code has to stay inside the width.
final ratio = count > 4 ? 1.20 : 1.05;
final double gap = MilerCodeBox.gap(context).w * (count > 4 ? 1 : 1.5);
return Row(
children: [
for (var i = 0; i < count; i++) ...[
if (i > 0) SizedBox(width: MilerCodeBox.gap(context).w),
Expanded(
child: AspectRatio(
aspectRatio: 1 / ratio,
child: _Box(
controller: controllers[i],
node: nodes[i],
obscure: obscure,
hasError: hasError,
onChanged: (v) => onChanged(v, i),
// A 4-digit PIN gets the full target; a 6-digit code starts smaller so it
// is not relying on the cap to be legible on a narrow phone.
final double preferred = (count > 4 ? 46 : 56).w;
return LayoutBuilder(
builder: (context, constraints) {
final double available = constraints.maxWidth - gap * (count - 1);
final double box = preferred < available / count
? preferred
: available / count;
// Barely taller than it is wide. A tall box puts the digit in the
// middle of empty space; this reads as a field.
final double height = box * 1.12;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < count; i++) ...[
if (i > 0) SizedBox(width: gap),
SizedBox(
width: box,
height: height,
child: _Box(
controller: controllers[i],
node: nodes[i],
obscure: obscure,
hasError: hasError,
onChanged: (v) => onChanged(v, i),
),
),
),
),
],
],
],
],
);
},
);
}
}

View File

@@ -1,5 +1,6 @@
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';
@@ -203,7 +204,7 @@ class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill {
TextSpan(
text: _maskedPhone,
style: TextStyle(
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
),
@@ -241,7 +242,7 @@ class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill {
child: canResend
? AuthTextAction(
label: 'Send a new code',
icon: Icons.refresh_rounded,
icon: LucideIcons.rotateCw,
onPressed: () async {
setState(() => _error = null);
_startTimer();