import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:sidak_desa_mobile/core/api/api.dart'; import 'konfirmasi_sandi_baru.dart'; class OtpVerificationPage extends StatefulWidget { final String email; const OtpVerificationPage({super.key, required this.email}); @override State createState() => _OtpVerificationPageState(); } class _OtpVerificationPageState extends State { final List otpControllers = List.generate( 5, (_) => TextEditingController(), ); final List focusNodes = List.generate( 5, (_) => FocusNode(), ); bool isLoading = false; static const Color primaryColor = Color(0xFF0B7D77); static const Color accentColor = Color(0xFFFF8C32); static const Color backgroundColor = Color(0xFFF4F7F8); static const Color textDarkColor = Color(0xFF1E293B); static const Color textSoftColor = Color(0xFF64748B); String get otpCode { return otpControllers.map((controller) => controller.text).join(); } @override void dispose() { for (final controller in otpControllers) { controller.dispose(); } for (final node in focusNodes) { node.dispose(); } super.dispose(); } Future verifyOtp() async { final otpString = otpCode; if (otpString.length < 5) { showMessage( title: 'OTP Belum Lengkap', message: 'Masukkan kode OTP 5 digit terlebih dahulu.', ); return; } setState(() => isLoading = true); try { final response = await http.post( Uri.parse('${Apiconfig.baseUrl}/api/verify-otp'), headers: const { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: jsonEncode({ 'email': widget.email, 'otp': otpString, }), ); final data = jsonDecode(response.body); if (!mounted) return; setState(() => isLoading = false); if (response.statusCode == 200) { Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => ResetPasswordPage(email: widget.email), ), ); } else { showMessage( title: 'OTP Salah', message: data['message'] ?? 'Kode OTP yang Anda masukkan tidak sesuai.', ); } } catch (e) { if (!mounted) return; setState(() => isLoading = false); showMessage( title: 'Koneksi Bermasalah', message: 'Gagal terhubung ke server. Silakan coba lagi.', ); } } void showMessage({ required String title, required String message, }) { showDialog( context: context, barrierDismissible: false, builder: (_) => Dialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), child: Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 76, height: 76, decoration: const BoxDecoration( color: Color.fromRGBO(244, 67, 54, 0.10), shape: BoxShape.circle, ), child: const Icon( Icons.warning_amber_rounded, color: Colors.red, size: 42, ), ), const SizedBox(height: 20), Text( title, textAlign: TextAlign.center, style: const TextStyle( fontSize: 19, fontWeight: FontWeight.w800, color: textDarkColor, ), ), const SizedBox(height: 10), Text( message, textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, height: 1.5, color: textSoftColor, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: accentColor, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), ), child: const Text( 'Kembali', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, ), ), ), ), ], ), ), ), ); } Widget otpBox(int index) { return SizedBox( width: 52, height: 58, child: TextField( controller: otpControllers[index], focusNode: focusNodes[index], keyboardType: TextInputType.number, maxLength: 1, textAlign: TextAlign.center, cursorColor: primaryColor, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w800, color: textDarkColor, ), decoration: InputDecoration( counterText: '', filled: true, fillColor: const Color(0xFFF8FAFC), contentPadding: EdgeInsets.zero, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: const BorderSide( color: Color(0xFFE2E8F0), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: const BorderSide( color: primaryColor, width: 1.5, ), ), ), onChanged: (value) { if (value.isNotEmpty && index < 4) { focusNodes[index + 1].requestFocus(); } if (value.isEmpty && index > 0) { focusNodes[index - 1].requestFocus(); } }, ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: backgroundColor, body: SafeArea( child: Stack( children: [ Container( height: 290, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF0B7D77), Color(0xFF0FA39A), ], ), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(36), bottomRight: Radius.circular(36), ), ), ), SingleChildScrollView( padding: const EdgeInsets.fromLTRB(22, 16, 22, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ if (Navigator.canPop(context)) InkWell( onTap: () => Navigator.pop(context), borderRadius: BorderRadius.circular(14), child: Container( width: 42, height: 42, decoration: BoxDecoration( color: const Color.fromRGBO(255, 255, 255, 0.18), borderRadius: BorderRadius.circular(14), ), child: const Icon( Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 18, ), ), ), ], ), const SizedBox(height: 18), Column( children: [ Container( width: 250, height: 96, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: const [ BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.12), blurRadius: 22, offset: Offset(0, 10), ), ], ), child: Image.asset( 'assets/images/logo.png', fit: BoxFit.contain, ), ), const SizedBox(height: 22), const Text( 'Verifikasi OTP', textAlign: TextAlign.center, style: TextStyle( fontSize: 26, height: 1.2, fontWeight: FontWeight.w800, color: Colors.white, ), ), const SizedBox(height: 10), Text( 'Akun: ${widget.email}', textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, color: Color.fromRGBO(255, 255, 255, 0.88), ), ), ], ), const SizedBox(height: 34), Container( padding: const EdgeInsets.all(22), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: const [ BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.08), blurRadius: 24, offset: Offset(0, 12), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Masukkan Kode OTP', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: textDarkColor, ), ), const SizedBox(height: 6), const Text( 'Isi 5 digit kode OTP yang telah dikirim ke email Anda.', style: TextStyle( fontSize: 13, height: 1.5, color: textSoftColor, ), ), const SizedBox(height: 26), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(5, otpBox), ), const SizedBox(height: 18), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color.fromRGBO(11, 125, 119, 0.08), borderRadius: BorderRadius.circular(16), border: Border.all( color: const Color.fromRGBO(11, 125, 119, 0.12), ), ), child: const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.info_outline_rounded, color: primaryColor, size: 20, ), SizedBox(width: 10), Expanded( child: Text( 'Jangan bagikan kode OTP kepada siapa pun demi keamanan akun Anda.', style: TextStyle( fontSize: 12.5, height: 1.45, color: textSoftColor, ), ), ), ], ), ), const SizedBox(height: 26), SizedBox( height: 54, child: ElevatedButton( onPressed: isLoading ? null : verifyOtp, style: ElevatedButton.styleFrom( backgroundColor: accentColor, disabledBackgroundColor: const Color.fromRGBO(255, 140, 50, 0.55), foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), child: isLoading ? const SizedBox( height: 22, width: 22, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2.4, ), ) : const Text( 'Verifikasi OTP', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w800, ), ), ), ), ], ), ), const SizedBox(height: 22), const Text( 'Setelah OTP benar, Anda akan diarahkan ke halaman pembuatan kata sandi baru.', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, height: 1.5, color: textSoftColor, ), ), ], ), ), ], ), ), ); } }