import 'dart:async'; 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/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 _otpControllers = List.generate( 6, (_) => TextEditingController(), ); final List _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 _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.w800, 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: Icons.refresh_rounded, 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, ), ), ], ); } }