MIF_E31222656/lib/screens/auth/reset_password_otp_screen.dart

362 lines
13 KiB
Dart

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<ResetPasswordOtpScreen> createState() => _ResetPasswordOtpScreenState();
}
class _ResetPasswordOtpScreenState extends State<ResetPasswordOtpScreen> {
final _formKey = GlobalKey<FormState>();
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<void> _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<void> _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<void> _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<void> _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),
],
),
),
);
}
}