306 lines
7.9 KiB
Dart
306 lines
7.9 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../../services/auth_service.dart';
|
|
import 'login_screen.dart';
|
|
|
|
class VerifyOtpScreen extends StatefulWidget {
|
|
final String email;
|
|
|
|
const VerifyOtpScreen({super.key, required this.email});
|
|
|
|
@override
|
|
State<VerifyOtpScreen> createState() => _VerifyOtpScreenState();
|
|
}
|
|
|
|
class _VerifyOtpScreenState extends State<VerifyOtpScreen> {
|
|
final List<TextEditingController> controllers = List.generate(
|
|
6,
|
|
(_) => TextEditingController(),
|
|
);
|
|
|
|
final List<FocusNode> focusNodes = List.generate(6, (_) => FocusNode());
|
|
|
|
bool isLoading = false;
|
|
bool isResendLoading = false;
|
|
|
|
int resendTimer = 60;
|
|
Timer? timer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
startTimer();
|
|
}
|
|
|
|
void startTimer() {
|
|
resendTimer = 60;
|
|
|
|
timer = Timer.periodic(const Duration(seconds: 1), (t) {
|
|
if (resendTimer == 0) {
|
|
t.cancel();
|
|
} else {
|
|
setState(() {
|
|
resendTimer--;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
String getOtp() {
|
|
return controllers.map((e) => e.text.trim()).join();
|
|
}
|
|
|
|
void verifyOtp() async {
|
|
String otp = getOtp();
|
|
|
|
if (otp.length != 6) {
|
|
_showSnackBar('OTP harus 6 digit', Colors.orange);
|
|
return;
|
|
}
|
|
|
|
setState(() => isLoading = true);
|
|
|
|
final result = await AuthService.verifyOtp(email: widget.email, otp: otp);
|
|
|
|
setState(() => isLoading = false);
|
|
|
|
if (!mounted) return;
|
|
|
|
if (result['success']) {
|
|
_showSnackBar('OTP valid! Silahkan login.', Colors.green);
|
|
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
|
);
|
|
} else {
|
|
_showSnackBar(result['message'] ?? 'OTP salah', Colors.red);
|
|
}
|
|
}
|
|
|
|
void resendOtp() async {
|
|
setState(() {
|
|
isResendLoading = true;
|
|
});
|
|
|
|
final result = await AuthService.resendOtp(email: widget.email);
|
|
|
|
setState(() {
|
|
isResendLoading = false;
|
|
});
|
|
|
|
if (result['success']) {
|
|
_showSnackBar("OTP berhasil dikirim ulang", Colors.green);
|
|
startTimer();
|
|
} else {
|
|
_showSnackBar(result['message'], Colors.red);
|
|
}
|
|
}
|
|
|
|
void onOtpChanged(int index, String value) {
|
|
/// Jika user paste OTP (misal 123456)
|
|
if (value.length > 1) {
|
|
final otpChars = value.split('');
|
|
|
|
for (int i = 0; i < otpChars.length && i < 6; i++) {
|
|
controllers[i].text = otpChars[i];
|
|
}
|
|
|
|
focusNodes[5].requestFocus();
|
|
|
|
if (otpChars.length == 6) {
|
|
verifyOtp();
|
|
}
|
|
|
|
setState(() {});
|
|
return;
|
|
}
|
|
|
|
/// Pindah ke kotak berikutnya
|
|
if (value.isNotEmpty) {
|
|
if (index < 5) {
|
|
focusNodes[index + 1].requestFocus();
|
|
}
|
|
}
|
|
/// Jika dihapus kembali ke kotak sebelumnya
|
|
else {
|
|
if (index > 0) {
|
|
focusNodes[index - 1].requestFocus();
|
|
}
|
|
}
|
|
|
|
String otp = getOtp();
|
|
|
|
if (otp.length == 6) {
|
|
verifyOtp();
|
|
}
|
|
}
|
|
|
|
Widget buildOtpBox(int index) {
|
|
return SizedBox(
|
|
width: 45,
|
|
child: TextField(
|
|
controller: controllers[index],
|
|
focusNode: focusNodes[index],
|
|
keyboardType: TextInputType.number,
|
|
textAlign: TextAlign.center,
|
|
maxLength: 1,
|
|
|
|
autofillHints: const [AutofillHints.oneTimeCode],
|
|
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
|
|
decoration: InputDecoration(
|
|
counterText: "",
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
|
|
onChanged: (value) {
|
|
onOtpChanged(index, value);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showSnackBar(String msg, Color color) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(msg),
|
|
backgroundColor: color,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
timer?.cancel();
|
|
|
|
for (var c in controllers) {
|
|
c.dispose();
|
|
}
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF5F7FA),
|
|
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Image.asset('assets/images/logopolije.png', height: 100),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
const Text(
|
|
"Verifikasi OTP",
|
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
|
),
|
|
|
|
const Text(
|
|
"Masukkan kode OTP yang dikirim ke email Anda",
|
|
style: TextStyle(color: Colors.grey),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: List.generate(6, buildOtpBox),
|
|
),
|
|
|
|
const SizedBox(height: 25),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 55,
|
|
|
|
child: ElevatedButton(
|
|
onPressed: isLoading ? null : verifyOtp,
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF2F5BEA),
|
|
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
|
|
child:
|
|
isLoading
|
|
? const CircularProgressIndicator(
|
|
color: Colors.white,
|
|
)
|
|
: const Text(
|
|
"Verifikasi OTP",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 25),
|
|
|
|
resendTimer > 0
|
|
? Text(
|
|
"Kirim ulang OTP dalam $resendTimer detik",
|
|
style: const TextStyle(color: Colors.grey),
|
|
)
|
|
: isResendLoading
|
|
? const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: GestureDetector(
|
|
onTap: resendOtp,
|
|
child: const Text(
|
|
"Kirim ulang OTP",
|
|
style: TextStyle(
|
|
color: Color(0xFF2F5BEA),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|