import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../../common/theme/theme.dart'; /// Input 6 digit dengan tampilan kotak. Nilainya tidak pernah disimpan di luar /// [controller]; pemanggil wajib mengosongkannya setelah dipakai. class PinCodeField extends StatefulWidget { const PinCodeField({ super.key, required this.controller, this.onCompleted, this.obscure = true, this.hasError = false, this.enabled = true, this.autofocus = true, }); final TextEditingController controller; final ValueChanged? onCompleted; final bool obscure; final bool hasError; final bool enabled; final bool autofocus; static const int length = 6; @override State createState() => _PinCodeFieldState(); } class _PinCodeFieldState extends State { final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); widget.controller.addListener(_onChanged); _focusNode.addListener(() => setState(() {})); } @override void didUpdateWidget(covariant PinCodeField oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { oldWidget.controller.removeListener(_onChanged); widget.controller.addListener(_onChanged); } } @override void dispose() { widget.controller.removeListener(_onChanged); _focusNode.dispose(); super.dispose(); } void _onChanged() { setState(() {}); final value = widget.controller.text; if (value.length == PinCodeField.length) { widget.onCompleted?.call(value); } } @override Widget build(BuildContext context) { final value = widget.controller.text; return GestureDetector( onTap: () => _focusNode.requestFocus(), child: Stack( children: [ // Field asli tak terlihat yang menerima input keyboard. Opacity( opacity: 0, child: SizedBox( height: 52, child: TextField( controller: widget.controller, focusNode: _focusNode, enabled: widget.enabled, autofocus: widget.autofocus, keyboardType: TextInputType.number, maxLength: PinCodeField.length, enableSuggestions: false, autocorrect: false, enableInteractiveSelection: false, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration(counterText: ''), ), ), ), IgnorePointer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(PinCodeField.length, (index) { final filled = index < value.length; final active = widget.enabled && _focusNode.hasFocus && index == value.length; return Container( width: 44, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: widget.enabled ? AppColor.white : AppColor.backgroundGradient.first, borderRadius: BorderRadius.circular(10), border: Border.all( width: active || widget.hasError ? 2 : 1, color: widget.hasError ? AppColor.error : active ? AppColor.primary : AppColor.border, ), ), child: filled ? widget.obscure ? Container( width: 12, height: 12, decoration: const BoxDecoration( color: AppColor.textPrimary, shape: BoxShape.circle, ), ) : Text( value[index], style: AppStyle.xl.copyWith( fontWeight: FontWeight.w700, ), ) : null, ); }), ), ), ], ), ); } }