152 lines
4.8 KiB
Dart
152 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
|
|
const Color primaryBrown = Color(0xFF4A3C3C);
|
|
|
|
/// Reusable 5-digit verification code input widget
|
|
class VerificationCodeInput extends StatefulWidget {
|
|
final List<TextEditingController> controllers;
|
|
final bool isError;
|
|
final VoidCallback? onErrorReset;
|
|
final bool enabled;
|
|
|
|
const VerificationCodeInput({
|
|
super.key,
|
|
required this.controllers,
|
|
this.isError = false,
|
|
this.onErrorReset,
|
|
this.enabled = true,
|
|
});
|
|
|
|
@override
|
|
State<VerificationCodeInput> createState() => _VerificationCodeInputState();
|
|
|
|
/// Get the combined code from all controllers
|
|
String get code => controllers.map((c) => c.text).join();
|
|
|
|
/// Check if all 5 digits are entered
|
|
bool get isComplete => code.length == 5;
|
|
}
|
|
|
|
class _VerificationCodeInputState extends State<VerificationCodeInput> {
|
|
late List<FocusNode> _focusNodes;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_focusNodes = List.generate(5, (index) => FocusNode());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (var node in _focusNodes) {
|
|
node.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: List.generate(
|
|
5,
|
|
(index) => SizedBox(
|
|
width: 50,
|
|
height: 50,
|
|
child: RawKeyboardListener(
|
|
focusNode: FocusNode(), // Separate dummy node for listener
|
|
onKey: (event) {
|
|
// Handle backspace on empty field to move focus back
|
|
if (event is RawKeyDownEvent &&
|
|
event.logicalKey == LogicalKeyboardKey.backspace) {
|
|
if (widget.controllers[index].text.isEmpty && index > 0) {
|
|
_focusNodes[index - 1].requestFocus();
|
|
}
|
|
}
|
|
},
|
|
child: TextField(
|
|
controller: widget.controllers[index],
|
|
focusNode: _focusNodes[index],
|
|
textAlign: TextAlign.center,
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 1,
|
|
enabled: widget.enabled,
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: primaryBrown,
|
|
),
|
|
decoration: InputDecoration(
|
|
counterText: "",
|
|
contentPadding: EdgeInsets.zero,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: widget.isError
|
|
? AppTheme.error
|
|
: primaryBrown.withOpacity(0.3),
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: widget.isError
|
|
? AppTheme.error
|
|
: primaryBrown.withOpacity(0.3),
|
|
),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: widget.isError ? AppTheme.error : primaryBrown,
|
|
width: 2,
|
|
),
|
|
),
|
|
disabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: primaryBrown.withOpacity(0.2)),
|
|
),
|
|
),
|
|
onChanged: (val) {
|
|
// Reset error state when user types
|
|
if (widget.isError && widget.onErrorReset != null) {
|
|
widget.onErrorReset!();
|
|
}
|
|
|
|
if (val.isNotEmpty) {
|
|
// Move to next field if current is filled
|
|
if (index < 4) {
|
|
_focusNodes[index + 1].requestFocus();
|
|
} else if (index == 4) {
|
|
// Hide keyboard on last field if filled
|
|
FocusScope.of(context).unfocus();
|
|
}
|
|
} else {
|
|
// Move to previous field if current is cleared
|
|
if (index > 0) {
|
|
_focusNodes[index - 1].requestFocus();
|
|
}
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Clear all controllers
|
|
void clear() {
|
|
for (var c in widget.controllers) {
|
|
c.clear();
|
|
}
|
|
// Return focus to first field after clearing
|
|
if (_focusNodes.isNotEmpty) {
|
|
_focusNodes[0].requestFocus();
|
|
}
|
|
}
|
|
}
|