MIF_E31222656/lib/screens/auth/register_screen.dart

777 lines
38 KiB
Dart

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tugas_akhir_supabase/widgets/legal_content_dialog.dart';
// Define theme colors
const Color primaryColor = Color(0xFF056839);
const Color secondaryColor = Color(0xFF4CAF50);
const Color backgroundColor = Color(0xFFF5F5F5);
const Color surfaceColor = Colors.white;
const Color textColor = Color(0xFF2D3748);
const Color subtextColor = Color(0xFF718096);
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
_RegisterScreenState createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _usernameController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _agreedToTerms = false;
Future<void> _register() async {
if (!_formKey.currentState!.validate()) return;
if (!_agreedToTerms) {
_showErrorSnackbar('Harap setujui Syarat & Ketentuan untuk melanjutkan');
return;
}
setState(() => _isLoading = true);
try {
final email = _emailController.text.trim();
final password = _passwordController.text.trim();
final username = _usernameController.text.trim();
final AuthResponse res = await Supabase.instance.client.auth.signUp(
email: email,
password: password,
);
final user = res.user;
if (user == null)
throw Exception(
'Otentikasi pengguna berhasil dibuat tetapi data tidak tersimpan',
);
if (res.session != null) {
await Supabase.instance.client.auth.setSession(
res.session!.accessToken,
);
}
try {
await _createProfileDirect(user.id, email, username);
} catch (e) {
debugPrint('Pembuatan profil langsung gagal: $e');
await _createProfileViaRpc(user.id, email, username);
}
if (!mounted) return;
Navigator.pushReplacementNamed(
context,
'/otp',
arguments: {'email': email, 'userId': user.id},
);
} catch (e) {
debugPrint('Kesalahan registrasi: $e');
String errorMessage = e.toString();
if (e is AuthException && e.message.isNotEmpty) {
errorMessage = e.message;
} else if (e is PostgrestException && e.message.isNotEmpty) {
errorMessage = e.message;
}
debugPrint('Error detail: $errorMessage');
_showErrorSnackbar(errorMessage);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _createProfileDirect(
String userId,
String email,
String username,
) async {
await Supabase.instance.client.from('profiles').insert({
'user_id': userId,
'username': username,
'email': email,
'created_at': DateTime.now().toUtc().toIso8601String(),
});
}
Future<void> _createProfileViaRpc(
String userId,
String email,
String username,
) async {
await Supabase.instance.client.rpc(
'create_profile',
params: {'p_user_id': userId, 'p_email': email, 'p_username': username},
);
}
void _showErrorSnackbar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'Tutup',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 20),
// Logo and welcome text
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Image.asset(
'assets/images/farm_logo.png',
errorBuilder:
(context, error, stackTrace) => Icon(
Icons.eco_rounded,
color: primaryColor,
size: 50,
),
),
),
),
const SizedBox(height: 32),
Text(
'Bergabung dengan TaniSM4RT',
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: primaryColor,
fontSize: 24,
),
),
const SizedBox(height: 12),
Text(
'Solusi pertanian cerdas untuk petani modern',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: subtextColor,
fontSize: 16,
),
),
],
),
),
const SizedBox(height: 48),
// Registration form
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildTextField(
controller: _usernameController,
label: 'Nama Pengguna',
hintText: 'Masukkan nama pengguna Anda',
prefixIcon: Icons.person_outline,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama pengguna wajib diisi';
}
if (value.length < 3) {
return 'Minimal 3 karakter';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _emailController,
label: 'Email',
hintText: 'Masukkan alamat email Anda',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email wajib diisi';
}
final bool emailValid = RegExp(
r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+',
).hasMatch(value);
if (!emailValid) {
return 'Masukkan alamat email yang valid';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _passwordController,
label: 'Kata Sandi',
hintText: 'Buat kata sandi yang kuat',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
color: subtextColor,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Kata sandi wajib diisi';
}
if (value.length < 8) {
return 'Minimal 8 karakter';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _confirmPasswordController,
label: 'Konfirmasi Kata Sandi',
hintText: 'Masukkan ulang kata sandi Anda',
prefixIcon: Icons.lock_outline,
obscureText: _obscureConfirmPassword,
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword
? Icons.visibility_off
: Icons.visibility,
color: subtextColor,
),
onPressed: () {
setState(() {
_obscureConfirmPassword =
!_obscureConfirmPassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Konfirmasi kata sandi wajib diisi';
}
if (value != _passwordController.text) {
return 'Kata sandi tidak cocok';
}
return null;
},
),
const SizedBox(height: 24),
// Terms and conditions
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Transform.scale(
scale: 0.9,
child: Checkbox(
value: _agreedToTerms,
onChanged: (value) {
setState(() {
_agreedToTerms = value ?? false;
});
},
activeColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 3),
child: RichText(
text: TextSpan(
style: TextStyle(
color: textColor,
fontSize: 12,
),
children: [
const TextSpan(text: 'Saya setuju dengan '),
TextSpan(
text: 'Syarat & Ketentuan',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
showDialog(
context: context,
builder:
(
context,
) => const LegalContentDialog(
title:
'Syarat & Ketentuan',
contentWidgets: [
ParagraphText(
'Selamat datang di TaniSM4RT (Platform Pertanian Cerdas)\n\n'
'Dengan menggunakan aplikasi kami, Anda menyetujui ketentuan-ketentuan ini yang mengatur akses Anda ke layanan pertanian cerdas kami.',
),
SectionTitle(
'1. Gambaran Layanan',
),
ParagraphText(
'TaniSM4RT menyediakan wawasan pertanian bertenaga AI, pemantauan tanaman, prakiraan cuaca, dan alat manajemen lahan untuk mengoptimalkan operasi pertanian Anda.',
),
SectionTitle(
'2. Tanggung Jawab Pengguna',
),
ListItem(
'Memberikan data lahan dan tanaman yang akurat untuk rekomendasi optimal',
),
ListItem(
'Menggunakan layanan hanya untuk tujuan pertanian yang sah',
),
ListItem(
'Menjaga keamanan dan kerahasiaan akun',
),
ListItem(
'Mematuhi peraturan dan hukum pertanian setempat',
),
SectionTitle(
'3. Data & Privasi',
),
ParagraphText(
'Data pertanian Anda membantu meningkatkan model AI kami. Kami melindungi informasi Anda sesuai dengan Kebijakan Privasi kami dan tidak pernah membagikan data lahan sensitif tanpa persetujuan.',
),
SectionTitle(
'4. Ketersediaan Layanan',
),
ParagraphText(
'Kami berusaha mencapai waktu aktif 99,9% tetapi tidak dapat menjamin layanan tanpa gangguan. Data cuaca dan rekomendasi disediakan sebagai panduan - keputusan pertanian akhir tetap menjadi tanggung jawab Anda.',
),
SectionTitle(
'5. Kekayaan Intelektual',
),
ParagraphText(
'Platform TaniSM4RT, algoritma, dan konten dilindungi oleh hukum kekayaan intelektual. Anda tetap memiliki kepemilikan atas data lahan Anda.',
),
SectionTitle(
'6. Pembatasan Tanggung Jawab',
),
ParagraphText(
'Rekomendasi kami bersifat konsultatif. Kami tidak bertanggung jawab atas kerugian tanaman, kerusakan terkait cuaca, atau keputusan pertanian berdasarkan wawasan kami.',
),
SectionTitle(
'7. Pembaruan & Perubahan',
),
ParagraphText(
'Kami dapat memperbarui ketentuan ini secara berkala. Penggunaan berkelanjutan merupakan penerimaan terhadap ketentuan yang direvisi.',
),
ParagraphText(
'Hubungi kami: support@tanismart.com\nTanggal Berlaku: Januari 2025',
),
],
),
);
},
),
const TextSpan(text: ' dan '),
TextSpan(
text: 'Kebijakan Privasi',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
showDialog(
context: context,
builder:
(
context,
) => const LegalContentDialog(
title:
'Kebijakan Privasi',
contentWidgets: [
ParagraphText(
'Kebijakan Privasi TaniSM4RT\n\n'
'Kami menghargai privasi Anda dan berkomitmen untuk melindungi data pribadi dan pertanian Anda. Kebijakan ini menjelaskan bagaimana kami mengumpulkan, menggunakan, dan melindungi informasi Anda.',
),
SectionTitle(
'1. Informasi yang Kami Kumpulkan',
),
ParagraphText(
'Informasi Akun:',
),
ListItem(
'Nama, email, nomor telepon, dan detail profil',
),
ListItem(
'Nama pengguna dan kata sandi terenkripsi',
),
ParagraphText(
'Data Pertanian:',
),
ListItem(
'Lokasi lahan, ukuran, dan jenis tanah',
),
ListItem(
'Jenis tanaman, jadwal tanam, dan data panen',
),
ListItem(
'Data cuaca dan sensor (jika terhubung)',
),
ListItem(
'Aktivitas pertanian dan penggunaan input',
),
ParagraphText(
'Data Penggunaan:',
),
ListItem(
'Interaksi aplikasi dan penggunaan fitur',
),
ListItem(
'Informasi perangkat dan alamat IP',
),
ListItem(
'Data lokasi (dengan izin)',
),
SectionTitle(
'2. Bagaimana Kami Menggunakan Data Anda',
),
ListItem(
'Memberikan rekomendasi pertanian yang dipersonalisasi',
),
ListItem(
'Menghasilkan prakiraan cuaca dan peringatan',
),
ListItem(
'Meningkatkan model AI dan fitur platform',
),
ListItem(
'Mengirim pembaruan dan notifikasi penting',
),
ListItem(
'Memastikan keamanan platform dan mencegah penipuan',
),
SectionTitle(
'3. Pembagian Data',
),
ParagraphText(
'Kami tidak menjual data pribadi Anda. Kami dapat membagikan data teragregasi dan anonim untuk:',
),
ListItem(
'Penelitian pertanian dan wawasan industri',
),
ListItem(
'Peningkatan pemodelan cuaca dan tanaman',
),
ListItem(
'Kemitraan akademis (dengan persetujuan)',
),
ParagraphText(
'Kami membagikan data pribadi hanya ketika:',
),
ListItem(
'Diwajibkan oleh hukum atau peraturan',
),
ListItem(
'Diperlukan untuk penyedia layanan (hosting cloud, analitik)',
),
ListItem(
'Anda memberikan persetujuan eksplisit',
),
SectionTitle(
'4. Keamanan Data',
),
ParagraphText(
'Kami menerapkan langkah-langkah keamanan standar industri termasuk enkripsi, server aman, kontrol akses, dan audit keamanan rutin untuk melindungi informasi Anda.',
),
SectionTitle(
'5. Hak Anda',
),
ListItem(
'Mengakses dan mengunduh data Anda',
),
ListItem(
'Memperbaiki informasi yang tidak akurat',
),
ListItem(
'Menghapus akun dan data Anda',
),
ListItem(
'Berhenti berlangganan komunikasi pemasaran',
),
ListItem(
'Mengontrol berbagi lokasi',
),
SectionTitle(
'6. Penyimpanan Data',
),
ParagraphText(
'Kami menyimpan data Anda selama akun Anda aktif dan untuk periode yang wajar setelah penghapusan untuk mematuhi persyaratan hukum.',
),
SectionTitle(
'7. Privasi Anak-anak',
),
ParagraphText(
'Layanan kami tidak ditujukan untuk pengguna di bawah 16 tahun. Kami tidak secara sengaja mengumpulkan data dari anak-anak.',
),
SectionTitle(
'8. Hubungi Kami',
),
ParagraphText(
'Ada pertanyaan tentang privasi? Hubungi kami di:\nprivacy@tanismart.com\n\nTerakhir Diperbarui: Mei 2025',
),
],
),
);
},
),
],
),
),
),
),
],
),
const SizedBox(height: 24),
// Register button
ElevatedButton(
onPressed:
(_isLoading || !_agreedToTerms) ? null : _register,
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
shadowColor: primaryColor.withOpacity(0.5),
),
child:
_isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Buat Akun',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 24),
// Sign in option
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Sudah punya akun?',
style: TextStyle(color: subtextColor),
),
TextButton(
onPressed: () {
Navigator.pushReplacementNamed(context, '/login');
},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: const EdgeInsets.only(left: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
foregroundColor: primaryColor,
),
child: const Text(
'Masuk',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
],
),
),
],
),
),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required String hintText,
required IconData prefixIcon,
bool obscureText = false,
Widget? suffixIcon,
String? Function(String?)? validator,
TextInputType keyboardType = TextInputType.text,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: textColor,
),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 12,
offset: const Offset(0, 3),
),
],
),
child: TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(
color: subtextColor.withOpacity(0.7),
fontSize: 15,
),
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Icon(
prefixIcon,
color: primaryColor.withOpacity(0.8),
size: 22,
),
),
prefixIconConstraints: const BoxConstraints(
minWidth: 50,
minHeight: 50,
),
suffixIcon:
suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: 8),
child: suffixIcon,
)
: null,
filled: true,
fillColor: surfaceColor,
contentPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: primaryColor.withOpacity(0.6),
width: 1.5,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: Colors.red.shade300, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: Colors.red.shade300, width: 1.5),
),
),
validator: validator,
),
),
],
);
}
}