TIFNJK_E41222758/lib/presentation/views/pages/veryfikasiOTP.dart

483 lines
16 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:sidak_desa_mobile/core/api/api.dart';
import 'konfirmasi_sandi_baru.dart';
class OtpVerificationPage extends StatefulWidget {
final String email;
const OtpVerificationPage({super.key, required this.email});
@override
State<OtpVerificationPage> createState() => _OtpVerificationPageState();
}
class _OtpVerificationPageState extends State<OtpVerificationPage> {
final List<TextEditingController> otpControllers = List.generate(
5,
(_) => TextEditingController(),
);
final List<FocusNode> focusNodes = List.generate(
5,
(_) => FocusNode(),
);
bool isLoading = false;
static const Color primaryColor = Color(0xFF0B7D77);
static const Color accentColor = Color(0xFFFF8C32);
static const Color backgroundColor = Color(0xFFF4F7F8);
static const Color textDarkColor = Color(0xFF1E293B);
static const Color textSoftColor = Color(0xFF64748B);
String get otpCode {
return otpControllers.map((controller) => controller.text).join();
}
@override
void dispose() {
for (final controller in otpControllers) {
controller.dispose();
}
for (final node in focusNodes) {
node.dispose();
}
super.dispose();
}
Future<void> verifyOtp() async {
final otpString = otpCode;
if (otpString.length < 5) {
showMessage(
title: 'OTP Belum Lengkap',
message: 'Masukkan kode OTP 5 digit terlebih dahulu.',
);
return;
}
setState(() => isLoading = true);
try {
final response = await http.post(
Uri.parse('${Apiconfig.baseUrl}/api/verify-otp'),
headers: const {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: jsonEncode({
'email': widget.email,
'otp': otpString,
}),
);
final data = jsonDecode(response.body);
if (!mounted) return;
setState(() => isLoading = false);
if (response.statusCode == 200) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => ResetPasswordPage(email: widget.email),
),
);
} else {
showMessage(
title: 'OTP Salah',
message: data['message'] ?? 'Kode OTP yang Anda masukkan tidak sesuai.',
);
}
} catch (e) {
if (!mounted) return;
setState(() => isLoading = false);
showMessage(
title: 'Koneksi Bermasalah',
message: 'Gagal terhubung ke server. Silakan coba lagi.',
);
}
}
void showMessage({
required String title,
required String message,
}) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 76,
height: 76,
decoration: const BoxDecoration(
color: Color.fromRGBO(244, 67, 54, 0.10),
shape: BoxShape.circle,
),
child: const Icon(
Icons.warning_amber_rounded,
color: Colors.red,
size: 42,
),
),
const SizedBox(height: 20),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 10),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Kembali',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
),
);
}
Widget otpBox(int index) {
return SizedBox(
width: 52,
height: 58,
child: TextField(
controller: otpControllers[index],
focusNode: focusNodes[index],
keyboardType: TextInputType.number,
maxLength: 1,
textAlign: TextAlign.center,
cursorColor: primaryColor,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: EdgeInsets.zero,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: primaryColor,
width: 1.5,
),
),
),
onChanged: (value) {
if (value.isNotEmpty && index < 4) {
focusNodes[index + 1].requestFocus();
}
if (value.isEmpty && index > 0) {
focusNodes[index - 1].requestFocus();
}
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: Stack(
children: [
Container(
height: 290,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0B7D77),
Color(0xFF0FA39A),
],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(36),
bottomRight: Radius.circular(36),
),
),
),
SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(22, 16, 22, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
if (Navigator.canPop(context))
InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(14),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 0.18),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.arrow_back_ios_new_rounded,
color: Colors.white,
size: 18,
),
),
),
],
),
const SizedBox(height: 18),
Column(
children: [
Container(
width: 250,
height: 96,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.12),
blurRadius: 22,
offset: Offset(0, 10),
),
],
),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.contain,
),
),
const SizedBox(height: 22),
const Text(
'Verifikasi OTP',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26,
height: 1.2,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
const SizedBox(height: 10),
Text(
'Akun: ${widget.email}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: Color.fromRGBO(255, 255, 255, 0.88),
),
),
],
),
const SizedBox(height: 34),
Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.08),
blurRadius: 24,
offset: Offset(0, 12),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Masukkan Kode OTP',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 6),
const Text(
'Isi 5 digit kode OTP yang telah dikirim ke email Anda.',
style: TextStyle(
fontSize: 13,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 26),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(5, otpBox),
),
const SizedBox(height: 18),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color.fromRGBO(11, 125, 119, 0.08),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color.fromRGBO(11, 125, 119, 0.12),
),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline_rounded,
color: primaryColor,
size: 20,
),
SizedBox(width: 10),
Expanded(
child: Text(
'Jangan bagikan kode OTP kepada siapa pun demi keamanan akun Anda.',
style: TextStyle(
fontSize: 12.5,
height: 1.45,
color: textSoftColor,
),
),
),
],
),
),
const SizedBox(height: 26),
SizedBox(
height: 54,
child: ElevatedButton(
onPressed: isLoading ? null : verifyOtp,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
disabledBackgroundColor:
const Color.fromRGBO(255, 140, 50, 0.55),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.4,
),
)
: const Text(
'Verifikasi OTP',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
),
),
],
),
),
const SizedBox(height: 22),
const Text(
'Setelah OTP benar, Anda akan diarahkan ke halaman pembuatan kata sandi baru.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
height: 1.5,
color: textSoftColor,
),
),
],
),
),
],
),
),
);
}
}