comprehensive cases

This commit is contained in:
2026-03-17 15:21:06 +05:30
parent e3d8d30b1b
commit 68b0055cfe
30 changed files with 1285 additions and 227 deletions

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:core_localization/core_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinput/pinput.dart';
import 'package:smart_auth/smart_auth.dart';
import '../../../blocs/auth_event.dart';
import '../../../blocs/auth_bloc.dart';
@@ -29,144 +31,98 @@ class OtpInputField extends StatefulWidget {
}
class _OtpInputFieldState extends State<OtpInputField> {
final List<TextEditingController> _controllers = List<TextEditingController>.generate(
6,
(int _) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List<FocusNode>.generate(6, (int _) => FocusNode());
/// Hidden field for E2E: Maestro inputText sends full OTP in one call;
/// the 6 visible boxes have maxLength:1 and would truncate.
late final FocusNode _hiddenFocusNode;
late final TextEditingController _controller;
late final SmartAuth _smartAuth;
@override
void initState() {
super.initState();
_hiddenFocusNode = FocusNode();
_controller = TextEditingController();
_smartAuth = SmartAuth();
_listenForSmsCode();
}
@override
void dispose() {
_hiddenFocusNode.dispose();
for (final TextEditingController controller in _controllers) {
controller.dispose();
}
for (final FocusNode node in _focusNodes) {
node.dispose();
}
_controller.dispose();
super.dispose();
}
/// Distributes full OTP from hidden field to the 6 visible boxes and notifies Bloc.
void _syncFromHidden(BuildContext context, String value) {
final String raw = value.replaceAll(RegExp(r'\D'), '');
final String digits = raw.length > 6 ? raw.substring(0, 6) : raw;
for (int i = 0; i < 6; i++) {
final String digit = i < digits.length ? digits[i] : '';
if (_controllers[i].text != digit) {
_controllers[i].text = digit;
}
}
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(digits));
if (digits.length == 6) {
widget.onCompleted(digits);
Future<void> _listenForSmsCode() async {
final res = await _smartAuth.getSmsCode();
if (res.code != null && mounted) {
_controller.text = res.code!;
_onChanged(_controller.text);
}
}
/// Helper getter to compute the current OTP code from all controllers.
String get _otpCode => _controllers.map((TextEditingController c) => c.text).join();
/// Handles changes to the OTP input fields.
void _onChanged({
required BuildContext context,
required int index,
required String value,
}) {
if (value.length == 1 && index < 5) {
_focusNodes[index + 1].requestFocus();
}
void _onChanged(String value) {
// Notify the Bloc of the change
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(_otpCode));
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(value));
if (_otpCode.length == 6) {
widget.onCompleted(_otpCode);
if (value.length == 6) {
widget.onCompleted(value);
}
}
@override
Widget build(BuildContext context) {
const double boxWidth = 45;
const double boxHeight = 56;
final defaultPinTheme = PinTheme(
width: 45,
height: 56,
textStyle: UiTypography.headline3m,
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.border,
width: 2,
),
),
);
final focusedPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
width: 2,
),
),
);
final submittedPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
width: 2,
),
),
);
final errorPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(color: UiColors.textError, width: 2),
),
);
return Column(
children: <Widget>[
SizedBox(
width: 300,
height: boxHeight,
child: Stack(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List<Widget>.generate(6, (int index) {
final TextField field = TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
maxLength: 1,
style: UiTypography.headline3m,
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
),
onChanged: (String value) =>
_onChanged(context: context, index: index, value: value),
);
return SizedBox(width: boxWidth, height: boxHeight, child: field);
}),
),
Positioned.fill(
child: Semantics(
identifier: 'staff_otp_input',
container: false,
child: Opacity(
opacity: 0.01,
child: TextField(
focusNode: _hiddenFocusNode,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6),
],
maxLength: 6,
onChanged: (String value) =>
_syncFromHidden(context, value),
),
),
),
),
],
child: Semantics(
identifier: 'staff_otp_input',
child: Pinput(
length: 6,
controller: _controller,
defaultPinTheme: defaultPinTheme,
focusedPinTheme: focusedPinTheme,
submittedPinTheme: submittedPinTheme,
errorPinTheme: errorPinTheme,
followingPinTheme: defaultPinTheme,
forceErrorState: widget.error.isNotEmpty,
onChanged: _onChanged,
onCompleted: widget.onCompleted,
autofillHints: const <String>[AutofillHints.oneTimeCode],
),
),
),
if (widget.error.isNotEmpty)

View File

@@ -18,6 +18,8 @@ dependencies:
firebase_auth: ^6.1.2
firebase_data_connect: ^0.2.2+1
http: ^1.2.0
pinput: ^5.0.0
smart_auth: ^1.1.0
# Architecture Packages
krow_domain: