import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:pin_code_fields/pin_code_fields.dart'; import 'dart:async'; class ResetPasswordOtpScreen extends StatefulWidget { final String email; const ResetPasswordOtpScreen({super.key, required this.email}); @override State createState() => _ResetPasswordOtpScreenState(); } class _ResetPasswordOtpScreenState extends State { final _formKey = GlobalKey(); final _otpController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); bool _isLoading = false; bool _otpVerified = false; String _enteredOtp = ''; int _resendCooldown = 60; late Timer _timer; bool _obscurePassword = true; bool _obscureConfirmPassword = true; final supabase = Supabase.instance.client; final Color primaryColor = const Color(0xFF056839); @override void initState() { super.initState(); _startCooldown(); _requestOtp(); } void _startCooldown() { _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) { timer.cancel(); return; } if (_resendCooldown > 0) { setState(() => _resendCooldown--); } else { timer.cancel(); } }); } Future _requestOtp() async { setState(() => _isLoading = true); try { await supabase.auth.resetPasswordForEmail(widget.email); _showSuccessSnackbar('Kode OTP telah dikirim ke email Anda.'); } catch (error) { _showErrorSnackbar('Gagal mengirim kode OTP. Silakan coba lagi nanti.'); } finally { if (mounted) setState(() => _isLoading = false); } } Future _verifyOtp() async { if (_enteredOtp.length != 6) { _showErrorSnackbar('Kode OTP harus 6 digit.'); return; } setState(() => _isLoading = true); try { final AuthResponse res = await supabase.auth.verifyOTP( email: widget.email, token: _enteredOtp, type: OtpType.recovery, ); if (res.session != null) { setState(() => _otpVerified = true); _showSuccessSnackbar('OTP terverifikasi. Silakan atur password baru Anda.'); } else { _showErrorSnackbar('Kode OTP tidak valid atau telah kedaluwarsa.'); } } catch (error) { _showErrorSnackbar('Verifikasi OTP gagal. Mohon pastikan kode benar dan coba lagi.'); } finally { if (mounted) setState(() => _isLoading = false); } } Future _resetPassword() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { await supabase.auth.updateUser( UserAttributes(password: _passwordController.text), ); _showSuccessSnackbar('Password berhasil diubah!'); if (mounted) { Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false); } } catch (error) { _showErrorSnackbar('Gagal mengubah password. Silakan coba lagi nanti.'); } finally { if (mounted) setState(() => _isLoading = false); } } Future _resendOtp() async { setState(() { _resendCooldown = 60; _isLoading = true; }); _startCooldown(); try { await supabase.auth.resetPasswordForEmail(widget.email); if (!mounted) return; _showSuccessSnackbar('Kode OTP baru telah dikirim ulang.'); _otpController.clear(); setState(() => _enteredOtp = ''); } catch (error) { if (!mounted) return; _showErrorSnackbar('Gagal mengirim ulang OTP. Mohon coba beberapa saat lagi.'); } 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, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), duration: const Duration(seconds: 3), ), ); } @override void dispose() { _timer.cancel(); _otpController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } Widget _buildOtpVerification(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 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:', 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: (value) { if (mounted) { setState(() => _enteredOtp = value); } }, beforeTextPaste: (text) => true, autoDisposeControllers: false, ), 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, 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), ), ), ], ); } Widget _buildPasswordReset(BuildContext context) { final theme = Theme.of(context); return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Atur Password Baru Anda', textAlign: TextAlign.center, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 24), TextFormField( controller: _passwordController, decoration: InputDecoration( hintText: 'Password Baru', prefixIcon: Icon(Icons.lock_outline, color: primaryColor), suffixIcon: IconButton( icon: Icon(_obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: primaryColor), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: primaryColor, width: 2)), contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), ), obscureText: _obscurePassword, validator: (val) { if (val == null || val.isEmpty) return 'Password tidak boleh kosong'; if (val.length < 6) return 'Password minimal 6 karakter'; return null; }, style: const TextStyle(color: Colors.black87), ), const SizedBox(height: 16), TextFormField( controller: _confirmPasswordController, decoration: InputDecoration( hintText: 'Konfirmasi Password Baru', prefixIcon: Icon(Icons.lock_outline, color: primaryColor), suffixIcon: IconButton( icon: Icon(_obscureConfirmPassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: primaryColor), onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword), ), filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: primaryColor, width: 2)), contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), ), obscureText: _obscureConfirmPassword, validator: (val) { if (val == null || val.isEmpty) return 'Konfirmasi password tidak boleh kosong'; if (val != _passwordController.text) return 'Password tidak cocok'; return null; }, style: const TextStyle(color: Colors.black87), ), 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 ? null : _resetPassword, child: _isLoading ? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3)) : const Text('Simpan Password', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], appBar: AppBar( title: Text( _otpVerified ? 'Atur Password Baru' : 'Verifikasi OTP', style: const 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), _otpVerified ? _buildPasswordReset(context) : _buildOtpVerification(context), ], ), ), ); } }