import 'dart:async'; import 'package:flutter/material.dart'; import 'package:pin_code_fields/pin_code_fields.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class OtpScreen extends StatefulWidget { final String email; final String userId; const OtpScreen({ super.key, required this.email, required this.userId, }); @override State createState() => _OtpScreenState(); } class _OtpScreenState extends State { final _otpController = TextEditingController(); bool _isLoading = false; int _resendCooldown = 60; String _enteredOtp = ''; late Timer _timer; final Color primaryColor = const Color(0xFF056839); // Dark Green from your branding @override void initState() { super.initState(); _startCooldown(); // Consider sending an initial OTP request here if not done prior to this screen WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { // Ensure the widget is still mounted _showInitialOtpInfo(); } }); } void _showInitialOtpInfo() { _showSuccessSnackbar('Kode OTP telah dikirim ke email Anda untuk verifikasi akun.'); } void _startCooldown() { _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) { timer.cancel(); return; } if (_resendCooldown > 0) { setState(() => _resendCooldown--); } else { timer.cancel(); } }); } Future _verifyOtp(String otp) async { if (!mounted) return; setState(() => _isLoading = true); try { final cleanOtp = otp.trim(); if (cleanOtp.length != 6) { _showErrorSnackbar('Kode OTP harus 6 digit.'); return; } final AuthResponse res = await Supabase.instance.client.auth.verifyOTP( email: widget.email, token: cleanOtp, type: OtpType.signup, ); if (res.session == null) { if (!mounted) return; _showErrorSnackbar('OTP tidak valid atau telah kedaluwarsa.'); return; } if (!mounted) return; _showSuccessSnackbar('Selamat Datang di Aplikasi TaniSM4RT!'); Navigator.of(context).pushNamedAndRemoveUntil( '/home', (Route route) => false, ); } catch (e) { String errorMessage = 'Verifikasi gagal. Silakan coba lagi.'; if (e is AuthException) { if (e.statusCode == '403' || e.message.contains('Token has expired or is invalid')) { errorMessage = 'Kode OTP telah kedaluwarsa atau tidak valid. Silakan minta kode baru.'; } else if (e.message.contains('Invalid OTP')) { errorMessage = 'Kode OTP tidak valid. Silakan coba lagi.'; } } else { errorMessage = 'Verifikasi gagal karena kesalahan teknis. Mohon coba lagi nanti.'; } _otpController.clear(); setState(() => _enteredOtp = ''); _showErrorSnackbar(errorMessage); } finally { if (mounted) setState(() => _isLoading = false); } } Future _resendOtp() async { if (!mounted) return; setState(() { _resendCooldown = 60; _isLoading = true; }); _startCooldown(); try { // Resend OTP for email verification await Supabase.instance.client.auth.resend( type: OtpType.signup, email: widget.email ); _showSuccessSnackbar('Kode OTP baru telah dikirim. Silakan cek email Anda (termasuk folder spam).'); _otpController.clear(); setState(() => _enteredOtp = ''); } catch (e) { String errorMessage = 'Gagal mengirim ulang OTP. Mohon coba lagi nanti.'; if (e is AuthException) { errorMessage = 'Gagal mengirim ulang OTP. Periksa koneksi atau coba beberapa saat lagi.'; } else { errorMessage = 'Gagal mengirim ulang OTP karena kesalahan teknis. Mohon coba lagi nanti.'; } _showErrorSnackbar(errorMessage); } finally { if (mounted) setState(() => _isLoading = false); } } void _showErrorSnackbar(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.redAccent, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), duration: const Duration(seconds: 3), ), ); } void _showSuccessSnackbar(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: primaryColor, // Use primaryColor for success behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), duration: const Duration(seconds: 3), ), ); } @override void dispose() { _timer.cancel(); _otpController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( backgroundColor: Colors.grey[100], appBar: AppBar( title: const Text( 'Verifikasi Akun', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), backgroundColor: primaryColor, elevation: 0, iconTheme: const IconThemeData(color: Colors.white), ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Image.asset('assets/images/logo.png', height: 100), const SizedBox(height: 32), Text( 'Verifikasi Email Anda', textAlign: TextAlign.center, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 12), Text( 'Kode verifikasi 6 digit telah dikirim ke email Anda:', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: Colors.black54), ), const SizedBox(height: 8), Text( widget.email, textAlign: TextAlign.center, style: theme.textTheme.bodyLarge?.copyWith( color: primaryColor, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 32), PinCodeTextField( appContext: context, length: 6, controller: _otpController, keyboardType: TextInputType.number, animationType: AnimationType.fade, pinTheme: PinTheme( shape: PinCodeFieldShape.box, borderRadius: BorderRadius.circular(12), fieldHeight: 50, fieldWidth: 45, activeFillColor: Colors.white, inactiveFillColor: Colors.white, selectedFillColor: Colors.white, activeColor: primaryColor, inactiveColor: Colors.grey.shade300, selectedColor: primaryColor, borderWidth: 1, ), enableActiveFill: true, onChanged: (value) { if (mounted) setState(() => _enteredOtp = value); }, onCompleted: (otp) { if (mounted) { setState(() => _enteredOtp = otp); _verifyOtp(otp); } }, beforeTextPaste: (text) => true, // Allow pasting autoDisposeControllers: false, // Important for manual controller management ), const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: primaryColor, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 2, ), onPressed: _isLoading || _enteredOtp.length != 6 ? null : () => _verifyOtp(_enteredOtp), child: _isLoading ? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3)) : const Text('Verifikasi Kode', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)), ), const SizedBox(height: 16), TextButton( onPressed: _resendCooldown > 0 || _isLoading ? null : _resendOtp, child: Text( _resendCooldown > 0 ? 'Kirim ulang OTP dalam $_resendCooldown detik' : 'Kirim Ulang OTP', style: TextStyle(color: _resendCooldown > 0 ? Colors.grey : primaryColor, fontWeight: FontWeight.bold), ), ), ], ), ), ); } }