first commit
This commit is contained in:
260
lib/views/onboardscreens/otp_page.dart
Normal file
260
lib/views/onboardscreens/otp_page.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
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<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.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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user