1+ import 'package:flutter/material.dart' ;
2+ import 'package:flutter/services.dart' ;
3+ import '../../core/theme/app_colors.dart' ;
4+ import '../viewmodels/auth_viewmodels.dart' ;
5+ import 'new_password_view.dart' ;
6+
7+ class OtpVerificationView extends StatefulWidget {
8+ const OtpVerificationView ({super .key});
9+
10+ @override
11+ State <OtpVerificationView > createState () => _OtpVerificationViewState ();
12+ }
13+
14+ class _OtpVerificationViewState extends State <OtpVerificationView > {
15+ // Bật chế độ 'chờ' cho ViewModel xử lý logic 8 số
16+ final _vm = OtpViewModel ();
17+
18+ @override
19+ Widget build (BuildContext context) {
20+ return Scaffold (
21+ backgroundColor: AppColors .background,
22+ appBar: AppBar (
23+ backgroundColor: Colors .transparent,
24+ elevation: 0 ,
25+ leading: IconButton (
26+ icon: const Icon (Icons .arrow_back, color: AppColors .primary),
27+ onPressed: () => Navigator .pop (context),
28+ ),
29+ title: const Text (
30+ 'Xác thực OTP' ,
31+ style: TextStyle (
32+ color: AppColors .primary,
33+ fontWeight: FontWeight .bold,
34+ fontSize: 18 ,
35+ ),
36+ ),
37+ centerTitle: true ,
38+ ),
39+ body: AnimatedBuilder (
40+ animation: _vm,
41+ builder: (context, child) {
42+ return SafeArea (
43+ child: SingleChildScrollView ( // Bọc lại đề phòng keyboard hiện lên làm tràn màn hình
44+ child: Padding (
45+ padding: const EdgeInsets .symmetric (horizontal: 20.0 , vertical: 40 ),
46+ child: Column (
47+ mainAxisAlignment: MainAxisAlignment .center,
48+ children: [
49+ const Icon (
50+ Icons .mark_email_read,
51+ size: 80 ,
52+ color: AppColors .primary,
53+ ),
54+ const SizedBox (height: 32 ),
55+ const Text (
56+ 'Nhập mã 8 số' , // Hiển thị đúng 8 số
57+ style: TextStyle (
58+ fontSize: 24 ,
59+ fontWeight: FontWeight .bold,
60+ color: AppColors .textDark,
61+ ),
62+ ),
63+ const SizedBox (height: 8 ),
64+ const Text (
65+ 'Mã đã được gửi đến email của bạn.' ,
66+ style: TextStyle (color: AppColors .textSecondary),
67+ ),
68+ const SizedBox (height: 40 ),
69+
70+ // --- KHU VỰC 8 Ô OTP (ĐÃ SỬA LỖI) ---
71+ // Dùng LayoutBuilder để tự tính toán kích thước ô cho vừa mọi màn hình
72+ LayoutBuilder (
73+ builder: (context, constraints) {
74+ // Tính toán độ rộng của ô dựa trên màn hình thật, trừ đi khoảng cách giữa các ô
75+ double availableWidth = constraints.maxWidth;
76+ double spaceBetweenBoxes = 6.0 ; // Khoảng cách giữa các ô
77+ double totalSpace = spaceBetweenBoxes * 7 ; // Có 7 khoảng trống giữa 8 ô
78+ double boxWidth = (availableWidth - totalSpace) / 8 ; // Độ rộng tối đa mỗi ô
79+
80+ // Khống chế độ rộng ô không quá to để nhìn cho art (max 35-40)
81+ double finalBoxWidth = boxWidth > 38 ? 38 : boxWidth;
82+
83+ return Row (
84+ mainAxisAlignment: MainAxisAlignment .center, // Căn giữa hàng 8 ô
85+ children: List .generate (
86+ 8 , // SỬA: Đã tạo đúng 8 ô ở đây
87+ (index) => Padding (
88+ padding: EdgeInsets .symmetric (horizontal: spaceBetweenBoxes / 2 ),
89+ child: _buildOtpBox (index, context, finalBoxWidth),
90+ ),
91+ ),
92+ );
93+ }
94+ ),
95+ const SizedBox (height: 40 ),
96+
97+ // Nút xác nhận xịn sò (Nhận lỗi cụ thể từ Server)
98+ ElevatedButton (
99+ onPressed: _vm.isLoading
100+ ? null
101+ : () async {
102+ FocusScope .of (context).unfocus ();
103+ // Gọi hàm verify(), nó trả về String? errorMessage
104+ final errorMessage = await _vm.verify ();
105+ if (! context.mounted) return ;
106+
107+ if (errorMessage == null ) {
108+ // Thành công: Nhảy sang bước 3 (Đổi mật khẩu mới)
109+ Navigator .pushReplacement (
110+ context,
111+ MaterialPageRoute (
112+ builder: (_) => const NewPasswordView (),
113+ ),
114+ );
115+ } else {
116+ // Thất bại: Hiện thông báo lỗi cụ thể (ví dụ: "Mã OTP hết hạn")
117+ ScaffoldMessenger .of (context).showSnackBar (
118+ SnackBar (
119+ content: Text (errorMessage),
120+ backgroundColor: AppColors .error,
121+ ),
122+ );
123+ }
124+ },
125+ style: ElevatedButton .styleFrom (
126+ backgroundColor: AppColors .primary,
127+ minimumSize: const Size (double .infinity, 56 ),
128+ shape: RoundedRectangleBorder (
129+ borderRadius: BorderRadius .circular (16 ),
130+ ),
131+ ),
132+ child: _vm.isLoading
133+ ? const CircularProgressIndicator (
134+ color: AppColors .white,
135+ )
136+ : const Text (
137+ 'XÁC NHẬN' ,
138+ style: TextStyle (
139+ fontSize: 16 ,
140+ fontWeight: FontWeight .bold,
141+ color: AppColors .white,
142+ ),
143+ ),
144+ ),
145+ const SizedBox (height: 16 ),
146+
147+ // --- NÚT GỬI LẠI MÃ (CHỈ CÓ Ở BẢN XỊN) ---
148+ TextButton .icon (
149+ onPressed: _vm.isLoading
150+ ? null
151+ : () async {
152+ final errorMessage = await _vm.resend ();
153+ if (! context.mounted) return ;
154+
155+ if (errorMessage == null ) {
156+ ScaffoldMessenger .of (context).showSnackBar (
157+ const SnackBar (
158+ content: Text ('Đã gửi lại mã OTP!' ),
159+ backgroundColor: AppColors .success,
160+ ),
161+ );
162+ } else {
163+ ScaffoldMessenger .of (context).showSnackBar (
164+ SnackBar (
165+ content: Text (errorMessage),
166+ backgroundColor: AppColors .error,
167+ ),
168+ );
169+ }
170+ },
171+ icon: const Icon (Icons .refresh, size: 18 , color: AppColors .primary),
172+ label: const Text (
173+ 'Gửi lại mã' ,
174+ style: TextStyle (color: AppColors .primary, fontWeight: FontWeight .bold),
175+ ),
176+ )
177+ ],
178+ ),
179+ ),
180+ ),
181+ );
182+ },
183+ ),
184+ );
185+ }
186+
187+ // SỬA: Nhận thêm 'boxWidth' để tự động co dãn cho vừa 8 ô
188+ Widget _buildOtpBox (int index, BuildContext context, double boxWidth) {
189+ return Container (
190+ width: boxWidth, // Sử dụng độ rộng đã tính toán
191+ height: 48 ,
192+ decoration: BoxDecoration (
193+ color: AppColors .white,
194+ borderRadius: BorderRadius .circular (8 ),
195+ border: Border .all (color: AppColors .border),
196+ ),
197+ child: TextField (
198+ onChanged: (value) {
199+ _vm.updateDigit (index, value);
200+ // SỬA: Logic nhảy focus chuẩn cho 8 ô (index chạy từ 0 đến 7)
201+ if (value.isNotEmpty && index < 7 ) {
202+ FocusScope .of (context).nextFocus ();
203+ }
204+ if (value.isEmpty && index > 0 ) {
205+ FocusScope .of (context).previousFocus ();
206+ }
207+ },
208+ style: const TextStyle (
209+ fontSize: 18 ,
210+ fontWeight: FontWeight .bold,
211+ color: AppColors .textDark,
212+ ),
213+ keyboardType: TextInputType .number,
214+ textAlign: TextAlign .center,
215+ inputFormatters: [
216+ LengthLimitingTextInputFormatter (1 ),
217+ FilteringTextInputFormatter .digitsOnly,
218+ ],
219+ decoration: const InputDecoration (
220+ border: InputBorder .none,
221+ counterText: '' ,
222+ ),
223+ ),
224+ );
225+ }
226+ }
0 commit comments