715 lines
23 KiB
Dart
715 lines
23 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:tabungankuy/app/modules/navbar/controllers/navbar_controller.dart';
|
|
import '../../../models/profil_model.dart';
|
|
import '../../../models/transaction_model.dart';
|
|
import '../../../models/target_emoji_map.dart';
|
|
import '../../../routes/app_pages.dart';
|
|
import '../../../services/notification_service.dart';
|
|
import '../../../widgets/open_celengan_modal.dart';
|
|
import '../../../widgets/numpad_pin_widget.dart';
|
|
import '../../../widgets/edit_target_modal.dart';
|
|
import '../../../widgets/konfirmasi_uang_modal.dart';
|
|
|
|
class HistoryItem {
|
|
final String amount;
|
|
final String date;
|
|
final String percent;
|
|
final String tipe;
|
|
|
|
const HistoryItem({
|
|
required this.amount,
|
|
required this.date,
|
|
required this.percent,
|
|
required this.tipe,
|
|
});
|
|
}
|
|
|
|
class HomeController extends GetxController {
|
|
final _auth = FirebaseAuth.instance;
|
|
final _firestore = FirebaseFirestore.instance;
|
|
final _rtdb = FirebaseDatabase.instance.ref();
|
|
|
|
final namaUser = ''.obs;
|
|
final targetEmoji = '🎯'.obs;
|
|
final targetLabel = ''.obs;
|
|
final targetPercent = 0.0.obs;
|
|
final targetNominal = 0.obs;
|
|
final totalSaldo = 0.obs;
|
|
final totalUang = 'Rp 0'.obs;
|
|
final isSaving = true.obs;
|
|
final isLoading = true.obs;
|
|
final recentHistory = <HistoryItem>[].obs;
|
|
|
|
final _pinController = TextEditingController();
|
|
final _editNominalController = TextEditingController();
|
|
final _editTargetController = TextEditingController();
|
|
final _pinWidgetKey = GlobalKey<NumpadPinInputWidgetState>();
|
|
|
|
int _pinAttempts = 0;
|
|
static const int _maxPinAttempts = 3;
|
|
|
|
StreamSubscription<DatabaseEvent>? _detectedValueSub;
|
|
StreamSubscription<DatabaseEvent>? _masukkanUangSub;
|
|
bool _konfirmasiModalTampil = false;
|
|
bool _pintuTerbuka = false;
|
|
bool _sudahNotifTargetTercapai = false;
|
|
bool _pinModalTerbuka = false;
|
|
|
|
Timer? _konfirmasiResetTimer;
|
|
int? _pendingNominal;
|
|
|
|
String get _uid => _auth.currentUser?.uid ?? '';
|
|
|
|
final _currencyFormat = NumberFormat.currency(
|
|
locale: 'id_ID',
|
|
symbol: 'Rp ',
|
|
decimalDigits: 0,
|
|
);
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
NotificationService.requestPermission();
|
|
_loadData();
|
|
_listenMasukkanUang();
|
|
_listenDetectedValue();
|
|
}
|
|
|
|
void _listenMasukkanUang() {
|
|
_masukkanUangSub = _rtdb.child('tabungan/masukkan_uang').onValue.listen((
|
|
event,
|
|
) {
|
|
final sebelumnya = _pintuTerbuka;
|
|
_pintuTerbuka = event.snapshot.value == true;
|
|
if (sebelumnya && !_pintuTerbuka) _cobaTampilkanKonfirmasi();
|
|
});
|
|
}
|
|
|
|
void _listenDetectedValue() {
|
|
_detectedValueSub = _rtdb.child('tabungan/detected_value').onValue.listen((
|
|
event,
|
|
) {
|
|
final value = event.snapshot.value;
|
|
if (value != null && value is int && value > 0) {
|
|
_pendingNominal = value;
|
|
_cobaTampilkanKonfirmasi();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _cobaTampilkanKonfirmasi() {
|
|
if (_pendingNominal == null || _pendingNominal! <= 0) return;
|
|
if (_pintuTerbuka || _konfirmasiModalTampil) return;
|
|
|
|
final context = Get.context;
|
|
if (context == null || !context.mounted) return;
|
|
|
|
_tampilkanKonfirmasiUang(context, _pendingNominal!);
|
|
}
|
|
|
|
void _tampilkanKonfirmasiUang(BuildContext context, int nominal) {
|
|
_konfirmasiModalTampil = true;
|
|
_pendingNominal = null;
|
|
|
|
KonfirmasiUangModal.show(
|
|
context,
|
|
nominal: nominal,
|
|
onKonfirmasi: () async {
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
_konfirmasiModalTampil = false;
|
|
_konfirmasiResetTimer?.cancel();
|
|
|
|
await _rtdb.child('tabungan').update({
|
|
'konfirmasi_masuk': true,
|
|
'detected_value': 0,
|
|
});
|
|
|
|
_konfirmasiResetTimer = Timer(const Duration(seconds: 5), () async {
|
|
if (!isClosed) {
|
|
await _rtdb.child('tabungan').update({'konfirmasi_masuk': false});
|
|
}
|
|
});
|
|
|
|
await _simpanTransaksiMasuk(nominal);
|
|
},
|
|
onBatal: () async {
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
_konfirmasiModalTampil = false;
|
|
_konfirmasiResetTimer?.cancel();
|
|
|
|
await _rtdb.child('tabungan').update({
|
|
'konfirmasi_masuk': false,
|
|
'detected_value': 0,
|
|
'masukkan_uang': true,
|
|
});
|
|
|
|
_pintuTerbuka = true;
|
|
Get.find<NavbarController>().isIotActive.value = true;
|
|
|
|
Get.snackbar(
|
|
'↩️ Dibatalkan',
|
|
'Pintu dibuka kembali, silakan coba lagi',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.orange.shade50,
|
|
colorText: Colors.orange.shade800,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _simpanTransaksiMasuk(int nominal) async {
|
|
try {
|
|
await _firestore.collection('transactions').add({
|
|
'userId': _uid,
|
|
'nominal': nominal,
|
|
'tipe': 'masuk',
|
|
'createdAt': FieldValue.serverTimestamp(),
|
|
});
|
|
await _loadData();
|
|
Get.snackbar(
|
|
'✅ Berhasil!',
|
|
'Berhasil menabung ${_currencyFormat.format(nominal)}',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.green.shade100,
|
|
colorText: Colors.green.shade800,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Gagal menyimpan transaksi: $e',
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
isLoading.value = true;
|
|
await _loadProfil();
|
|
await _loadTransaksi();
|
|
isLoading.value = false;
|
|
}
|
|
|
|
Future<void> _loadProfil() async {
|
|
try {
|
|
if (_uid.isEmpty) return;
|
|
final doc = await _firestore.collection('profil').doc(_uid).get();
|
|
if (!doc.exists) return;
|
|
|
|
final profil = ProfilModel.fromMap(doc.data()!);
|
|
namaUser.value = profil.nama;
|
|
targetLabel.value = profil.target;
|
|
targetEmoji.value = TargetEmojiMap.getEmoji(profil.target);
|
|
targetNominal.value = profil.targetNominal;
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Gagal memuat profil: $e',
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadTransaksi() async {
|
|
try {
|
|
if (_uid.isEmpty) return;
|
|
|
|
QuerySnapshot snapshot;
|
|
try {
|
|
snapshot = await _firestore
|
|
.collection('transactions')
|
|
.where('userId', isEqualTo: _uid)
|
|
.orderBy('createdAt', descending: true)
|
|
.get();
|
|
} catch (_) {
|
|
snapshot = await _firestore
|
|
.collection('transactions')
|
|
.where('userId', isEqualTo: _uid)
|
|
.get();
|
|
}
|
|
|
|
final docs = snapshot.docs.toList()
|
|
..sort((a, b) {
|
|
final aTime = (a.data() as Map)['createdAt'] as Timestamp?;
|
|
final bTime = (b.data() as Map)['createdAt'] as Timestamp?;
|
|
if (aTime == null && bTime == null) return 0;
|
|
if (aTime == null) return 1;
|
|
if (bTime == null) return -1;
|
|
return bTime.compareTo(aTime);
|
|
});
|
|
|
|
final transactions = docs
|
|
.map(
|
|
(doc) => TransactionModel.fromMap(
|
|
doc.id,
|
|
doc.data() as Map<String, dynamic>,
|
|
),
|
|
)
|
|
.toList();
|
|
|
|
int saldo = 0;
|
|
for (final t in transactions) {
|
|
saldo += t.tipe == 'masuk' ? t.nominal : -t.nominal;
|
|
}
|
|
|
|
totalSaldo.value = saldo;
|
|
totalUang.value = _currencyFormat.format(saldo);
|
|
|
|
final persenSebelumnya = targetPercent.value;
|
|
_hitungPersen();
|
|
_cekDanKirimNotifikasiTarget(persenSebelumnya);
|
|
|
|
recentHistory.value = transactions.take(2).map((t) {
|
|
final dateStr = t.createdAt != null
|
|
? DateFormat('hh:mm a dd MMM yyyy', 'id_ID').format(t.createdAt!)
|
|
: '-';
|
|
final persen = targetNominal.value > 0
|
|
? '${((t.nominal / targetNominal.value) * 100).toStringAsFixed(1)}%'
|
|
: '0%';
|
|
return HistoryItem(
|
|
amount: _currencyFormat.format(t.nominal),
|
|
date: dateStr,
|
|
percent: persen,
|
|
tipe: t.tipe,
|
|
);
|
|
}).toList();
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Gagal memuat transaksi: $e',
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _hitungPersen() {
|
|
targetPercent.value = targetNominal.value > 0
|
|
? ((totalSaldo.value / targetNominal.value) * 100).clamp(0.0, 100.0)
|
|
: 0.0;
|
|
}
|
|
|
|
void _cekDanKirimNotifikasiTarget(double persenSebelumnya) {
|
|
final sudahTercapai = targetPercent.value >= 100.0;
|
|
if (sudahTercapai &&
|
|
(persenSebelumnya < 100.0 || !_sudahNotifTargetTercapai)) {
|
|
if (!_sudahNotifTargetTercapai) {
|
|
_sudahNotifTargetTercapai = true;
|
|
NotificationService.showTargetTercapai(
|
|
namaTarget: targetLabel.value,
|
|
nominal: _currencyFormat.format(targetNominal.value),
|
|
);
|
|
}
|
|
}
|
|
if (!sudahTercapai) _sudahNotifTargetTercapai = false;
|
|
}
|
|
|
|
void toggleSaving() => isSaving.value = !isSaving.value;
|
|
|
|
void bukaCelengan(BuildContext context) {
|
|
OpenCelenganModal.show(
|
|
context,
|
|
targetTercapai: targetPercent.value >= 100.0,
|
|
onKonfirmasiBuka: () => _tampilkanInputPin(context),
|
|
);
|
|
}
|
|
|
|
void _tampilkanInputPin(BuildContext context) {
|
|
_pinController.clear();
|
|
_pinAttempts = 0;
|
|
_pinModalTerbuka = true;
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.white,
|
|
isScrollControlled: true,
|
|
isDismissible: false,
|
|
enableDrag: false,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
builder: (sheetContext) => NumpadPinInputWidget(
|
|
key: _pinWidgetKey,
|
|
displayController: _pinController,
|
|
onKey: _onPinKey,
|
|
onSimpan: () => _validasiPin(context),
|
|
onVerifikasiPassword: () => _tampilkanVerifikasiPassword(context),
|
|
),
|
|
).whenComplete(() => _pinModalTerbuka = false);
|
|
}
|
|
|
|
Future<void> _validasiPin(BuildContext context) async {
|
|
final pin = _pinController.text;
|
|
try {
|
|
if (_uid.isEmpty) return;
|
|
final doc = await _firestore.collection('profil').doc(_uid).get();
|
|
|
|
if (!doc.exists) {
|
|
_pinWidgetKey.currentState?.showError('Data profil tidak ditemukan');
|
|
return;
|
|
}
|
|
|
|
final savedPin = (doc.data()?['pin'] ?? '').toString();
|
|
if (pin != savedPin) {
|
|
_pinAttempts++;
|
|
final sisaPercobaan = _maxPinAttempts - _pinAttempts;
|
|
|
|
if (_pinAttempts >= _maxPinAttempts) {
|
|
_pinWidgetKey.currentState?.showError(
|
|
'PIN salah 3x, verifikasi dengan password akun',
|
|
showVerifikasiPassword: true,
|
|
);
|
|
} else {
|
|
_pinWidgetKey.currentState?.showError(
|
|
'PIN salah ($sisaPercobaan percobaan tersisa)',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
_pinAttempts = 0;
|
|
await _rtdb.child('tabungan').update({'buka_tabungan': true});
|
|
Get.back();
|
|
await Future.delayed(const Duration(milliseconds: 350));
|
|
await _simpanTransaksiAmbil(totalSaldo.value);
|
|
} catch (_) {
|
|
_pinWidgetKey.currentState?.showError('Terjadi kesalahan, coba lagi');
|
|
}
|
|
}
|
|
|
|
Future<void> _tampilkanVerifikasiPassword(BuildContext context) async {
|
|
final emailController = TextEditingController();
|
|
final passwordController = TextEditingController();
|
|
|
|
final verified = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (dialogContext) {
|
|
String errorMsg = '';
|
|
bool isLoading = false;
|
|
|
|
return StatefulBuilder(
|
|
builder: (ctx, setStateDialog) => Dialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
backgroundColor: Colors.white,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'Verifikasi Akun',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Masukkan email dan password akun untuk membuka celengan',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 14, color: Colors.black54),
|
|
),
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
controller: emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
autofocus: true,
|
|
decoration: InputDecoration(
|
|
hintText: 'Email akun',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 14,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: passwordController,
|
|
obscureText: true,
|
|
decoration: InputDecoration(
|
|
hintText: 'Password akun',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 14,
|
|
),
|
|
),
|
|
),
|
|
if (errorMsg.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
errorMsg,
|
|
style: TextStyle(
|
|
color: Colors.red.shade600,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
onPressed: () =>
|
|
Navigator.of(dialogContext).pop(false),
|
|
child: const Text('Batal'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF5B9BD5),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
onPressed: isLoading
|
|
? null
|
|
: () async {
|
|
final email = emailController.text.trim();
|
|
final password = passwordController.text
|
|
.trim();
|
|
|
|
if (email.isEmpty || password.isEmpty) {
|
|
setStateDialog(() {
|
|
errorMsg =
|
|
'Email dan password tidak boleh kosong';
|
|
});
|
|
return;
|
|
}
|
|
|
|
setStateDialog(() {
|
|
isLoading = true;
|
|
errorMsg = '';
|
|
});
|
|
|
|
try {
|
|
final user = _auth.currentUser;
|
|
if (user == null) {
|
|
throw Exception('User tidak ditemukan');
|
|
}
|
|
|
|
final credential =
|
|
EmailAuthProvider.credential(
|
|
email: email,
|
|
password: password,
|
|
);
|
|
await user.reauthenticateWithCredential(
|
|
credential,
|
|
);
|
|
Navigator.of(dialogContext).pop(true);
|
|
} on FirebaseAuthException catch (e) {
|
|
setStateDialog(() {
|
|
isLoading = false;
|
|
errorMsg = switch (e.code) {
|
|
'wrong-password' ||
|
|
'invalid-credential' =>
|
|
'Email atau password salah, coba lagi',
|
|
'user-mismatch' =>
|
|
'Email tidak sesuai dengan akun ini',
|
|
'invalid-email' =>
|
|
'Format email tidak valid',
|
|
'too-many-requests' =>
|
|
'Terlalu banyak percobaan, coba lagi nanti',
|
|
_ => 'Gagal verifikasi: ${e.message}',
|
|
};
|
|
});
|
|
} catch (_) {
|
|
setStateDialog(() {
|
|
isLoading = false;
|
|
errorMsg = 'Terjadi kesalahan, coba lagi';
|
|
});
|
|
}
|
|
},
|
|
child: isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Text(
|
|
'Verifikasi',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
emailController.dispose();
|
|
passwordController.dispose();
|
|
|
|
if (verified != true) return;
|
|
|
|
_pinAttempts = 0;
|
|
|
|
if (_pinModalTerbuka) {
|
|
Get.back();
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
}
|
|
|
|
await _rtdb.child('tabungan').update({'buka_tabungan': true});
|
|
await _simpanTransaksiAmbil(totalSaldo.value);
|
|
}
|
|
|
|
Future<void> _simpanTransaksiAmbil(int jumlah) async {
|
|
try {
|
|
await _firestore.collection('transactions').add({
|
|
'userId': _uid,
|
|
'nominal': jumlah,
|
|
'tipe': 'keluar',
|
|
'createdAt': FieldValue.serverTimestamp(),
|
|
});
|
|
|
|
Future.delayed(const Duration(seconds: 10), () async {
|
|
if (!isClosed) {
|
|
await _rtdb.child('tabungan').update({'buka_tabungan': false});
|
|
}
|
|
});
|
|
|
|
await _loadData();
|
|
|
|
Get.snackbar(
|
|
'✅ Berhasil!',
|
|
'Berhasil mengambil ${_currencyFormat.format(jumlah)} dari celengan',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.green.shade100,
|
|
colorText: Colors.green.shade800,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
final navContext = Get.context;
|
|
if (navContext != null && navContext.mounted) {
|
|
editTarget(navContext);
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Gagal menyimpan transaksi: $e',
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
}
|
|
}
|
|
|
|
void editTarget(BuildContext context) {
|
|
_editNominalController.text = targetNominal.value.toString();
|
|
_editTargetController.text = targetLabel.value;
|
|
|
|
EditTargetModal.show(
|
|
context,
|
|
nominalController: _editNominalController,
|
|
targetController: _editTargetController,
|
|
onNominalKey: _onEditNominalKey,
|
|
onSimpan: () async {
|
|
final newTarget = _editTargetController.text;
|
|
final newNominal =
|
|
int.tryParse(
|
|
_editNominalController.text.replaceAll(RegExp(r'[^0-9]'), ''),
|
|
) ??
|
|
0;
|
|
|
|
await _firestore.collection('profil').doc(_uid).update({
|
|
'target': newTarget,
|
|
'targetNominal': newNominal,
|
|
});
|
|
|
|
targetLabel.value = newTarget;
|
|
targetEmoji.value = TargetEmojiMap.getEmoji(newTarget);
|
|
targetNominal.value = newNominal;
|
|
_hitungPersen();
|
|
await _loadTransaksi();
|
|
Get.back();
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> refreshData() => _loadData();
|
|
|
|
Future<void> logout() async {
|
|
_detectedValueSub?.cancel();
|
|
_masukkanUangSub?.cancel();
|
|
_konfirmasiResetTimer?.cancel();
|
|
await _rtdb.child('tabungan').update({
|
|
'masukkan_uang': false,
|
|
'konfirmasi_masuk': false,
|
|
'buka_tabungan': false,
|
|
'detected_value': 0,
|
|
});
|
|
await _auth.signOut();
|
|
Get.offAllNamed(Routes.LOGIN);
|
|
}
|
|
|
|
void _onEditNominalKey(String key) {
|
|
if (key == 'del') {
|
|
if (_editNominalController.text.isNotEmpty) {
|
|
_editNominalController.text = _editNominalController.text.substring(
|
|
0,
|
|
_editNominalController.text.length - 1,
|
|
);
|
|
}
|
|
} else {
|
|
_editNominalController.text += key;
|
|
}
|
|
}
|
|
|
|
void _onPinKey(String key) {
|
|
if (key == 'del') {
|
|
if (_pinController.text.isNotEmpty) {
|
|
_pinController.text = _pinController.text.substring(
|
|
0,
|
|
_pinController.text.length - 1,
|
|
);
|
|
}
|
|
} else if (_pinController.text.length < 6) {
|
|
_pinController.text += key;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_detectedValueSub?.cancel();
|
|
_masukkanUangSub?.cancel();
|
|
_konfirmasiResetTimer?.cancel();
|
|
_pinController.dispose();
|
|
_editNominalController.dispose();
|
|
_editTargetController.dispose();
|
|
super.onClose();
|
|
}
|
|
}
|