feat: Implement major features and fix theming bug

This commit introduces several new features and a critical bug fix.

- Implemented a full "Quick Pay" flow for both within and outside the bank, including IFSC validation, beneficiary verification, and a TPIN-based payment process.
- Added a date range filter to the Account Statement screen and streamlined the UI by removing the amount filters.
- Fixed a major bug that prevented dynamic theme changes from being applied. The app now correctly switches between color themes.
- Refactored and improved beneficiary management, transaction models, and the fund transfer flow to support NEFT/RTGS.
This commit is contained in:
asif
2025-08-11 04:06:05 +05:30
parent 3024ddef15
commit f91d0f739b
34 changed files with 1638 additions and 911 deletions

View File

@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/api/services/beneficiary_service.dart';
@@ -8,15 +9,14 @@ import 'beneficiary_result_page.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
class AddBeneficiaryScreen extends StatefulWidget {
final List<User>? users;
final int? selectedIndex;
final String customerName;
const AddBeneficiaryScreen({
super.key,
this.users,
this.selectedIndex,
required this.customerName,
});
@override
@@ -26,7 +26,6 @@ class AddBeneficiaryScreen extends StatefulWidget {
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final _formKey = GlobalKey<FormState>();
late User selectedUser = (widget.users ?? [])[widget.selectedIndex!];
final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController =
TextEditingController();
@@ -35,13 +34,14 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final TextEditingController branchNameController = TextEditingController();
final TextEditingController ifscController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final service = getIt<BeneficiaryService>();
String? _beneficiaryName;
bool _isValidating = false;
bool _isBeneficiaryValidated = false;
String? _validationError;
late String accountType;
final String _selectedAccountType = 'Savings'; // default value
@override
void initState() {
@@ -53,167 +53,159 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
});
}
ifsc? _ifscData;
bool _isLoading = false; //for validateIFSC()
void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return;
setState(() {
_isLoading = true;
_ifscData = null;
});
void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return;
final result = await beneficiaryService.validateIFSC(ifsc);
setState(() {
_isLoading = false;
_ifscData = result;
});
if (result == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
);
bankNameController.clear();
branchNameController.clear();
} else {
print("${AppLocalizations.of(context).validIfsc}: ${result.bankName}, ${result.branchName}");
bankNameController.text = result.bankName;
branchNameController.text = result.branchName;
}
}
Future<void> _validateBeneficiary() async {
// start spinner / disable button
setState(() {
_isValidating = true;
_validationError = null;
_isBeneficiaryValidated = false;
nameController.text = ''; // clear previous name
});
final String accountNo = accountNumberController.text.trim();
final String ifsc = ifscController.text.trim();
final String remitter = selectedUser.name ?? '';
final service = getIt<BeneficiaryService>();
try {
// Step 1: call validate API -> get refNo
final String? refNo = await service.validateBeneficiary(
accountNo: accountNo,
ifscCode: ifsc,
remitterName: remitter,
);
if (refNo == null || refNo.isEmpty) {
setState(() {
_validationError = 'Validation request failed. Please check details.';
_isBeneficiaryValidated = false;
});
return;
}
// Step 2: poll checkValidationStatus for up to 30 seconds
const int timeoutSeconds = 30;
const int intervalSeconds = 2;
int elapsed = 0;
String? foundName;
while (elapsed < timeoutSeconds) {
final String? name = await service.checkValidationStatus(refNo);
if (name != null && name.trim().isNotEmpty) {
foundName = name.trim();
break;
if (mounted) {
if (result.bankName == '') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
);
bankNameController.clear();
branchNameController.clear();
} else {
bankNameController.text = result.bankName;
branchNameController.text = result.branchName;
}
await Future.delayed(const Duration(seconds: intervalSeconds));
elapsed += intervalSeconds;
}
}
void _validateBeneficiary() async {
FocusScope.of(context).unfocus();
setState(() {
_isValidating = true;
_validationError = null;
_isBeneficiaryValidated = false;
nameController.text = ''; // clear previous name
});
final String accountNo = accountNumberController.text.trim();
final String ifsc = ifscController.text.trim();
final String remitter = widget.customerName;
final service = getIt<BeneficiaryService>();
try {
final String beneficiaryName = await service.validateBeneficiary(
accountNo: accountNo,
ifscCode: ifsc,
remitterName: remitter,
);
if (foundName != null) {
setState(() {
nameController.text = foundName!;
nameController.text = beneficiaryName;
_isBeneficiaryValidated = true;
_validationError = null;
});
} else {
} catch (e) {
setState(() {
_validationError = 'Beneficiary not found within timeout.';
_validationError = e.toString();
_isBeneficiaryValidated = false;
});
}
} catch (e, st) {
// handle unexpected errors
// print or log if you want
debugPrint('Error validating beneficiary: $e\n$st');
setState(() {
_validationError = 'Something went wrong. Please try again.';
_isBeneficiaryValidated = false;
});
} finally {
if (mounted) {
setState(() {
_isValidating = false;
});
} finally {
if (mounted) {
setState(() {
_isValidating = false;
});
}
}
}
}
String _selectedAccountType = 'Savings'; // default value
void validateAndAddBeneficiary() async {
// Show spinner and disable UI
showDialog(
context: context,
barrierDismissible: false, // Prevent dismiss on tap outside
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async => false, // Disable back button
child: const Center(
child: CircularProgressIndicator(),
),
);
},
);
final beneficiary = Beneficiary(
accountNo: accountNumberController.text.trim(),
accountType: _selectedAccountType,
name: nameController.text.trim(),
ifscCode: ifscController.text.trim(),
);
var service = getIt<BeneficiaryService>();
try {
await service.sendForValidation(beneficiary);
bool isFound = await service.checkIfFound(beneficiary.accountNo);
if (context.mounted) {
Navigator.pop(context); // Close the spinner
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BeneficiaryResultPage(isSuccess: isFound),
),
);
void validateAndAddBeneficiary() async {
// First, validate the form fields (account number, confirm account, ifsc, etc.)
if (!_formKey.currentState!.validate()) {
return;
}
} catch (e) {
Navigator.pop(context); // Close the spinner
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).somethingWentWrong)),
// Ensure beneficiary is validated before proceeding to TPIN
if (!_isBeneficiaryValidated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please validate beneficiary details first.')),
);
return;
}
// Create the beneficiary object without TPIN for now
final beneficiaryWithoutTpin = Beneficiary(
accountNo: accountNumberController.text.trim(),
accountType: accountType,
name: nameController.text.trim(),
ifscCode: ifscController.text.trim(),
bankName: bankNameController.text.trim(),
branchName: branchNameController.text.trim(),
);
// Navigate to TPIN screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TransactionPinScreen(
onPinCompleted: (pinScreenContext, tpin) async {
// Show loading spinner while processing
showDialog(
context: pinScreenContext,
barrierDismissible: false,
builder: (BuildContext ctx) {
return WillPopScope(
onWillPop: () async => false,
child: const Center(
child: CircularProgressIndicator(),
),
);
},
);
// Create a new Beneficiary object with the TPIN
final beneficiaryWithTpin = Beneficiary(
accountNo: beneficiaryWithoutTpin.accountNo,
accountType: beneficiaryWithoutTpin.accountType,
name: beneficiaryWithoutTpin.name,
ifscCode: beneficiaryWithoutTpin.ifscCode,
bankName: beneficiaryWithoutTpin.bankName,
branchName: beneficiaryWithoutTpin.branchName,
tpin: tpin,
);
try {
final isSuccess = await service.sendForValidation(beneficiaryWithTpin);
if (pinScreenContext.mounted) {
Navigator.pop(pinScreenContext); // Close the spinner
Navigator.pushReplacement(
pinScreenContext,
MaterialPageRoute(
builder: (ctx) => BeneficiaryResultPage(isSuccess: isSuccess),
),
);
}
} on DioException catch (e) {
if (pinScreenContext.mounted) {
Navigator.pop(pinScreenContext); // Close the spinner
ScaffoldMessenger.of(pinScreenContext).showSnackBar(
SnackBar(
content: Text(e.response?.statusCode == 409
? 'Beneficiary already exists'
: 'Something went Wrong')),
);
}
} catch (e) {
if (pinScreenContext.mounted) {
Navigator.pop(pinScreenContext); // Close the spinner
ScaffoldMessenger.of(pinScreenContext).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).somethingWentWrong)),
);
}
}
},
),
),
);
}
}
@override
Widget build(BuildContext context) {
@@ -227,7 +219,8 @@ void validateAndAddBeneficiary() async {
),
title: Text(
AppLocalizations.of(context).addBeneficiary,
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
actions: [
@@ -265,10 +258,11 @@ void validateAndAddBeneficiary() async {
context,
).accountNumber,
// prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
@@ -282,6 +276,12 @@ void validateAndAddBeneficiary() async {
obscureText: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.length < 10) {
return AppLocalizations.of(
@@ -300,10 +300,11 @@ void validateAndAddBeneficiary() async {
context,
).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
@@ -339,7 +340,8 @@ void validateAndAddBeneficiary() async {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
@@ -377,7 +379,6 @@ void validateAndAddBeneficiary() async {
return null;
},
),
const SizedBox(height: 24),
// 🔹 Bank Name (Disabled)
TextFormField(
@@ -388,7 +389,8 @@ void validateAndAddBeneficiary() async {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor, // disabled color
fillColor: Theme.of(context)
.dialogBackgroundColor, // disabled color
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
@@ -400,7 +402,6 @@ void validateAndAddBeneficiary() async {
),
),
),
const SizedBox(height: 24),
// 🔹 Branch Name (Disabled)
TextFormField(
@@ -423,59 +424,62 @@ void validateAndAddBeneficiary() async {
),
),
),
//Validate Beneficiary Name
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating
? null
: () {
if (
confirmAccountNumberController.text ==
accountNumberController.text) {
_validateBeneficiary();
} else {
setState(() {
_validationError =
'Please enter a valid and matching account number.';
});
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Validate Beneficiary'),
),
),
),
const SizedBox(height: 24),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsetsGeometry.only(bottom: 24),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating
? null
: () {
if (confirmAccountNumberController
.text ==
accountNumberController.text) {
_validateBeneficiary();
} else {
setState(() {
_validationError =
'Please enter a valid and matching account number.';
});
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Text('Validate Beneficiary'),
),
),
),
//Beneficiary Name (Disabled)
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null || value.isEmpty
? AppLocalizations.of(context).nameRequired
: null,
),
controller: nameController,
enabled: false,
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null || value.isEmpty
? AppLocalizations.of(context).nameRequired
: null,
),
const SizedBox(height: 24),
// 🔹 Account Type Dropdown
DropdownButtonFormField<String>(
@@ -485,7 +489,8 @@ if (!_isBeneficiaryValidated)
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
@@ -496,18 +501,17 @@ if (!_isBeneficiaryValidated)
),
),
),
items:
[
AppLocalizations.of(context).savings,
AppLocalizations.of(context).current,
]
.map(
(type) => DropdownMenuItem(
value: type,
child: Text(type),
),
)
.toList(),
items: [
AppLocalizations.of(context).savings,
AppLocalizations.of(context).current,
]
.map(
(type) => DropdownMenuItem(
value: type,
child: Text(type),
),
)
.toList(),
onChanged: (value) {
setState(() {
accountType = value!;
@@ -525,7 +529,8 @@ if (!_isBeneficiaryValidated)
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
@@ -539,8 +544,8 @@ if (!_isBeneficiaryValidated)
textInputAction: TextInputAction.done,
validator: (value) =>
value == null || value.length != 10
? AppLocalizations.of(context).enterValidPhone
: null,
? AppLocalizations.of(context).enterValidPhone
: null,
),
const SizedBox(height: 35),
],
@@ -553,13 +558,13 @@ if (!_isBeneficiaryValidated)
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed:
validateAndAddBeneficiary,
onPressed: validateAndAddBeneficiary,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColorDark,
foregroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).validateAndAdd),
),