- Replaced the custom delete confirmation dialog with a reusable ConfirmDialog widget for better code organization and reusability. - Improved the add expense bottom sheet by implementing form validation using a GlobalKey and TextFormField. - Enhanced user experience by adding validation for required fields and specific formats (e.g., GST, transaction ID). - Updated the expense list to reflect changes in the confirmation dialog and improved the handling of attachments. - Cleaned up code by removing unnecessary comments and ensuring consistent formatting.
272 lines
9.8 KiB
Dart
272 lines
9.8 KiB
Dart
// lib/utils/validators.dart
|
||
import 'package:flutter/services.dart';
|
||
|
||
/// Common validators for Indian IDs, payments, and typical form fields.
|
||
class Validators {
|
||
// -----------------------------
|
||
// Regexes (compiled once)
|
||
// -----------------------------
|
||
static final RegExp _panRegex = RegExp(r'^[A-Z]{5}[0-9]{4}[A-Z]$');
|
||
// GSTIN: 2-digit/valid state code, PAN, entity code (1-9A-Z), 'Z', checksum (0-9A-Z)
|
||
static final RegExp _gstRegex = RegExp(
|
||
r'^(0[1-9]|1[0-9]|2[0-9]|3[0-7])[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$',
|
||
);
|
||
// Aadhaar digits only
|
||
static final RegExp _aadhaarRegex = RegExp(r'^[2-9]\d{11}$');
|
||
// Name (letters + spaces + dots + hyphen/apostrophe)
|
||
static final RegExp _nameRegex = RegExp(r"^[A-Za-z][A-Za-z .'\-]{1,49}$");
|
||
// Email (generic)
|
||
static final RegExp _emailRegex =
|
||
RegExp(r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$");
|
||
// Indian mobile
|
||
static final RegExp _mobileRegex = RegExp(r'^[6-9]\d{9}$');
|
||
// Pincode (India: 6 digits starting 1–9)
|
||
static final RegExp _pincodeRegex = RegExp(r'^[1-9][0-9]{5}$');
|
||
// IFSC (4 letters + 0 + 6 alphanumeric)
|
||
static final RegExp _ifscRegex = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
||
// Bank account number (9–18 digits)
|
||
static final RegExp _bankAccountRegex = RegExp(r'^\d{9,18}$');
|
||
// UPI ID (name@bank, simple check)
|
||
static final RegExp _upiRegex =
|
||
RegExp(r'^[\w.\-]{2,}@[\w]{2,}$', caseSensitive: false);
|
||
// Strong password (8+ chars, upper, lower, digit, special)
|
||
static final RegExp _passwordRegex =
|
||
RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$');
|
||
// Date dd/mm/yyyy (basic validation)
|
||
static final RegExp _dateRegex =
|
||
RegExp(r'^([0-2][0-9]|3[0-1])/(0[1-9]|1[0-2])/[0-9]{4}$');
|
||
// URL
|
||
static final RegExp _urlRegex = RegExp(
|
||
r'^(https?:\/\/)?([a-zA-Z0-9.-]+)\.[a-zA-Z]{2,}(:\d+)?(\/\S*)?$');
|
||
// Transaction ID (alphanumeric, dashes/underscores, 8–36 chars)
|
||
static final RegExp _transactionIdRegex =
|
||
RegExp(r'^[A-Za-z0-9\-_]{8,36}$');
|
||
|
||
// -----------------------------
|
||
// PAN
|
||
// -----------------------------
|
||
static bool isValidPAN(String? input) {
|
||
if (input == null) return false;
|
||
return _panRegex.hasMatch(input.trim().toUpperCase());
|
||
}
|
||
|
||
// -----------------------------
|
||
// GSTIN
|
||
// -----------------------------
|
||
static bool isValidGSTIN(String? input) {
|
||
if (input == null) return false;
|
||
return _gstRegex.hasMatch(_compact(input).toUpperCase());
|
||
}
|
||
|
||
// -----------------------------
|
||
// Aadhaar
|
||
// -----------------------------
|
||
static bool isValidAadhaar(String? input, {bool enforceChecksum = true}) {
|
||
if (input == null) return false;
|
||
final a = _digitsOnly(input);
|
||
if (!_aadhaarRegex.hasMatch(a)) return false;
|
||
return enforceChecksum ? _verhoeffValidate(a) : true;
|
||
}
|
||
|
||
// -----------------------------
|
||
// Mobile
|
||
// -----------------------------
|
||
static bool isValidIndianMobile(String? input) {
|
||
if (input == null) return false;
|
||
final s = _digitsOnly(input.replaceFirst(RegExp(r'^(?:\+?91|0)'), ''));
|
||
return _mobileRegex.hasMatch(s);
|
||
}
|
||
|
||
// -----------------------------
|
||
// Email
|
||
// -----------------------------
|
||
static bool isValidEmail(String? input, {bool gmailOnly = false}) {
|
||
if (input == null) return false;
|
||
final e = input.trim();
|
||
if (!_emailRegex.hasMatch(e)) return false;
|
||
if (!gmailOnly) return true;
|
||
final domain = e.split('@').last.toLowerCase();
|
||
return domain == 'gmail.com' || domain == 'googlemail.com';
|
||
}
|
||
|
||
static bool isValidGmail(String? input) =>
|
||
isValidEmail(input, gmailOnly: true);
|
||
|
||
// -----------------------------
|
||
// Name
|
||
// -----------------------------
|
||
static bool isValidName(String? input, {int minLen = 2, int maxLen = 50}) {
|
||
if (input == null) return false;
|
||
final s = input.trim();
|
||
if (s.length < minLen || s.length > maxLen) return false;
|
||
return _nameRegex.hasMatch(s);
|
||
}
|
||
|
||
// -----------------------------
|
||
// Transaction ID
|
||
// -----------------------------
|
||
static bool isValidTransactionId(String? input) {
|
||
if (input == null) return false;
|
||
return _transactionIdRegex.hasMatch(input.trim());
|
||
}
|
||
|
||
// -----------------------------
|
||
// Other fields
|
||
// -----------------------------
|
||
static bool isValidPincode(String? input) =>
|
||
input != null && _pincodeRegex.hasMatch(input.trim());
|
||
|
||
static bool isValidIFSC(String? input) =>
|
||
input != null && _ifscRegex.hasMatch(input.trim().toUpperCase());
|
||
|
||
static bool isValidBankAccount(String? input) =>
|
||
input != null && _bankAccountRegex.hasMatch(_digitsOnly(input));
|
||
|
||
static bool isValidUPI(String? input) =>
|
||
input != null && _upiRegex.hasMatch(input.trim());
|
||
|
||
static bool isValidPassword(String? input) =>
|
||
input != null && _passwordRegex.hasMatch(input.trim());
|
||
|
||
static bool isValidDate(String? input) =>
|
||
input != null && _dateRegex.hasMatch(input.trim());
|
||
|
||
static bool isValidURL(String? input) =>
|
||
input != null && _urlRegex.hasMatch(input.trim());
|
||
|
||
// -----------------------------
|
||
// Numbers
|
||
// -----------------------------
|
||
static bool isInt(String? input) =>
|
||
input != null && int.tryParse(input.trim()) != null;
|
||
|
||
static bool isDouble(String? input) =>
|
||
input != null && double.tryParse(input.trim()) != null;
|
||
|
||
static bool isNumeric(String? input) => isInt(input) || isDouble(input);
|
||
|
||
static bool isInRange(num? value,
|
||
{num? min, num? max, bool inclusive = true}) {
|
||
if (value == null) return false;
|
||
if (min != null && (inclusive ? value < min : value <= min)) return false;
|
||
if (max != null && (inclusive ? value > max : value >= max)) return false;
|
||
return true;
|
||
}
|
||
|
||
// -----------------------------
|
||
// Flutter-friendly validator lambdas (return null when valid)
|
||
// -----------------------------
|
||
static String? requiredField(String? v, {String fieldName = 'This field'}) =>
|
||
(v == null || v.trim().isEmpty) ? '$fieldName is required' : null;
|
||
|
||
static String? panValidator(String? v) =>
|
||
isValidPAN(v) ? null : 'Enter a valid PAN (e.g., ABCDE1234F)';
|
||
|
||
static String? gstValidator(String? v, {bool optional = false}) {
|
||
if (optional && (v == null || v.trim().isEmpty)) return null;
|
||
return isValidGSTIN(v) ? null : 'Enter a valid GSTIN';
|
||
}
|
||
|
||
static String? aadhaarValidator(String? v) =>
|
||
isValidAadhaar(v) ? null : 'Enter a valid Aadhaar (12 digits)';
|
||
|
||
static String? mobileValidator(String? v) =>
|
||
isValidIndianMobile(v) ? null : 'Enter a valid 10-digit mobile';
|
||
|
||
static String? emailValidator(String? v, {bool gmailOnly = false}) =>
|
||
isValidEmail(v, gmailOnly: gmailOnly)
|
||
? null
|
||
: gmailOnly
|
||
? 'Enter a valid Gmail address'
|
||
: 'Enter a valid email address';
|
||
|
||
static String? nameValidator(String? v, {int minLen = 2, int maxLen = 50}) =>
|
||
isValidName(v, minLen: minLen, maxLen: maxLen)
|
||
? null
|
||
: 'Enter a valid name ($minLen–$maxLen chars)';
|
||
|
||
static String? transactionIdValidator(String? v) =>
|
||
isValidTransactionId(v)
|
||
? null
|
||
: 'Enter a valid Transaction ID (8–36 chars, letters/numbers)';
|
||
|
||
static String? pincodeValidator(String? v) =>
|
||
isValidPincode(v) ? null : 'Enter a valid 6-digit pincode';
|
||
|
||
static String? ifscValidator(String? v) =>
|
||
isValidIFSC(v) ? null : 'Enter a valid IFSC code';
|
||
|
||
static String? bankAccountValidator(String? v) =>
|
||
isValidBankAccount(v) ? null : 'Enter a valid bank account (9–18 digits)';
|
||
|
||
static String? upiValidator(String? v) =>
|
||
isValidUPI(v) ? null : 'Enter a valid UPI ID';
|
||
|
||
static String? passwordValidator(String? v) =>
|
||
isValidPassword(v)
|
||
? null
|
||
: 'Password must be 8+ chars with upper, lower, digit, special';
|
||
|
||
static String? dateValidator(String? v) =>
|
||
isValidDate(v) ? null : 'Enter date in dd/mm/yyyy format';
|
||
|
||
static String? urlValidator(String? v) =>
|
||
isValidURL(v) ? null : 'Enter a valid URL';
|
||
|
||
// -----------------------------
|
||
// Helpers
|
||
// -----------------------------
|
||
static String _digitsOnly(String s) => s.replaceAll(RegExp(r'\D'), '');
|
||
static String _compact(String s) => s.replaceAll(RegExp(r'\s'), '');
|
||
|
||
// -----------------------------
|
||
// Verhoeff checksum (for Aadhaar)
|
||
// -----------------------------
|
||
static const List<List<int>> _verhoeffD = [
|
||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
|
||
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
|
||
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
|
||
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
|
||
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
|
||
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
|
||
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
|
||
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
|
||
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
|
||
];
|
||
static const List<List<int>> _verhoeffP = [
|
||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
|
||
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
|
||
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
|
||
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
|
||
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
|
||
[2, 7, 9, 3, 8, 0, 5, 4, 1, 6],
|
||
[7, 0, 4, 6, 9, 1, 2, 3, 5, 8],
|
||
];
|
||
|
||
static bool _verhoeffValidate(String numStr) {
|
||
int c = 0;
|
||
final rev = numStr.split('').reversed.map(int.parse).toList();
|
||
for (int i = 0; i < rev.length; i++) {
|
||
c = _verhoeffD[c][_verhoeffP[(i % 8)][rev[i]]];
|
||
}
|
||
return c == 0;
|
||
}
|
||
}
|
||
|
||
/// Common input formatters/masks useful in TextFields.
|
||
class InputFormatters {
|
||
static final digitsOnly = FilteringTextInputFormatter.digitsOnly;
|
||
static final upperAlnum =
|
||
FilteringTextInputFormatter.allow(RegExp(r'[A-Z0-9]'));
|
||
static final upperLetters =
|
||
FilteringTextInputFormatter.allow(RegExp(r'[A-Z]'));
|
||
static final name =
|
||
FilteringTextInputFormatter.allow(RegExp(r"[A-Za-z .'\-]"));
|
||
static final alnumWithSpace =
|
||
FilteringTextInputFormatter.allow(RegExp(r"[A-Za-z0-9 ]"));
|
||
static LengthLimitingTextInputFormatter maxLen(int n) =>
|
||
LengthLimitingTextInputFormatter(n);
|
||
}
|