MIF_E31222656/lib/screens/auth/otp_screen.dart

278 lines
9.1 KiB
Dart

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<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends State<OtpScreen> {
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<void> _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<dynamic> 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<void> _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),
),
),
],
),
),
);
}
}