stripe_native_card_field/lib/card_details.dart

309 lines
7.5 KiB
Dart
Raw Normal View History

2023-11-14 09:58:49 -07:00
import 'package:flutter/foundation.dart';
class CardDetails {
CardDetails({
required dynamic cardNumber,
required String? securityCode,
required this.expirationString,
required this.postalCode,
}) : _cardNumber = cardNumber {
2023-11-14 09:58:49 -07:00
this.securityCode = int.tryParse(securityCode ?? '');
checkIsValid();
}
factory CardDetails.blank() {
return CardDetails(cardNumber: null, securityCode: null, expirationString: null, postalCode: null);
2023-11-14 09:58:49 -07:00
}
String? get cardNumber => _cardNumber?.replaceAll(' ', '');
set cardNumber(String? num) => _cardNumber = num;
String? _cardNumber;
2023-11-14 09:58:49 -07:00
int? securityCode;
String? postalCode;
String? expirationString;
DateTime? expirationDate;
2023-11-14 09:58:49 -07:00
bool _complete = false;
ValidState _validState = ValidState.blank;
int _lastCheckHash = 0;
CardProvider? provider;
ValidState get validState {
checkIsValid();
return _validState;
}
bool get cardNumberFilled =>
_cardNumber == null ? false : (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length;
2023-11-14 09:58:49 -07:00
bool get isComplete {
checkIsValid();
return _complete;
}
int get minInnLength => 1;
int get maxINNLength => 4;
void checkIsValid() {
try {
int currentHash = hash;
if (currentHash == _lastCheckHash) {
return;
}
_lastCheckHash = currentHash;
if (_cardNumber == null && expirationString == null && securityCode == null && postalCode == null) {
2023-11-14 09:58:49 -07:00
_complete = false;
_validState = ValidState.blank;
return;
}
final nums = _cardNumber!
2023-11-14 09:58:49 -07:00
.replaceAll(' ', '')
.split('')
.map(
(i) => int.parse(i),
)
.toList();
if (!luhnAlgorithmCheck(nums)) {
_complete = false;
_validState = ValidState.invalidCard;
return;
}
if (_cardNumber == null || !cardNumberFilled) {
2023-11-14 09:58:49 -07:00
_complete = false;
_validState = ValidState.missingCard;
return;
}
if (expirationString == null) {
2023-11-14 09:58:49 -07:00
_complete = false;
_validState = ValidState.missingDate;
return;
}
final expSplits = expirationString!.split('/');
2023-11-14 09:58:49 -07:00
if (expSplits.length != 2 || expSplits.last == '') {
_complete = false;
_validState = ValidState.missingDate;
return;
}
final month = int.parse(expSplits.first[0] == '0' ? expSplits.first[1] : expSplits.first);
if (month < 1 || month > 12) {
_complete = false;
_validState = ValidState.invalidMonth;
return;
}
final year = 2000 + int.parse(expSplits.last);
final date = DateTime(year, month);
2023-11-14 09:58:49 -07:00
if (date.isBefore(DateTime.now())) {
_complete = false;
_validState = ValidState.dateTooEarly;
return;
} else if (date.isAfter(DateTime.now().add(const Duration(days: 365 * 50)))) {
_complete = false;
_validState = ValidState.dateTooLate;
return;
}
expirationDate = date;
2023-11-14 09:58:49 -07:00
if (securityCode == null) {
_complete = false;
_validState = ValidState.missingCVC;
return;
}
if (postalCode == null) {
_complete = false;
_validState = ValidState.missingZip;
return;
}
if (!RegExp(r'^\d{5}(-\d{4})?$').hasMatch(postalCode!)) {
_complete = false;
_validState = ValidState.invalidZip;
return;
}
_complete = true;
_validState = ValidState.ok;
} catch (err, st) {
if (kDebugMode) {
print('Error while validating CardDetails: $err\n$st');
}
_complete = false;
_validState = ValidState.error;
}
}
int get hash {
return Object.hash(_cardNumber, expirationString, securityCode, postalCode);
2023-11-14 09:58:49 -07:00
}
void detectCardProvider() {
bool found = false;
if (_cardNumber == null) {
2023-11-14 09:58:49 -07:00
return;
}
for (var cardPvd in providers) {
if (cardPvd.innValidNums != null) {
2023-11-14 09:58:49 -07:00
// trim card number to correct length
String trimmedNum = _cardNumber!;
String innNumStr = '${cardPvd.innValidNums!.first}';
2023-11-14 09:58:49 -07:00
if (trimmedNum.length > innNumStr.length) {
trimmedNum = trimmedNum.substring(0, innNumStr.length);
}
final num = int.tryParse(trimmedNum);
if (num == null) continue;
if (cardPvd.innValidNums!.contains(num)) {
2023-11-14 09:58:49 -07:00
provider = cardPvd;
found = true;
break;
}
}
if (cardPvd.innValidRanges != null) {
2023-11-14 09:58:49 -07:00
// trim card number to correct length
String trimmedNum = _cardNumber!;
String innNumStr = '${cardPvd.innValidRanges!.first.low}';
2023-11-14 09:58:49 -07:00
if (trimmedNum.length > innNumStr.length) {
trimmedNum = trimmedNum.substring(0, innNumStr.length);
}
final num = int.tryParse(trimmedNum);
if (num == null) continue;
if (cardPvd.innValidRanges!.any((range) => range.isWithin(num))) {
2023-11-14 09:58:49 -07:00
provider = cardPvd;
found = true;
break;
}
}
}
if (!found) provider = null;
}
@override
String toString() {
return 'Number: "$_cardNumber" - Exp: "$expirationString" CVC: $securityCode Zip: "$postalCode"';
}
2023-11-14 09:58:49 -07:00
}
enum ValidState {
ok,
error,
blank,
missingCard,
invalidCard,
missingDate,
invalidMonth,
2023-11-14 09:58:49 -07:00
dateTooEarly,
dateTooLate,
missingCVC,
invalidCVC,
missingZip,
invalidZip,
}
enum CardProviderID {
americanExpress,
dinersClub,
discoverCard,
mastercard,
jcb,
visa,
2023-11-14 09:58:49 -07:00
}
class CardProvider {
CardProviderID id;
List<int>? innValidNums;
List<Range>? innValidRanges;
2023-11-14 09:58:49 -07:00
int cardLength;
int cvcLength;
CardProvider(
{required this.id, required this.cardLength, required this.cvcLength, this.innValidNums, this.innValidRanges}) {
2023-11-14 09:58:49 -07:00
// Must provide one or the other
assert(innValidNums != null || innValidRanges != null);
2023-11-14 09:58:49 -07:00
// Do not provide empty list of valid nums
assert(innValidNums == null || innValidNums!.isNotEmpty);
2023-11-14 09:58:49 -07:00
}
@override
String toString() {
return id.toString();
}
}
class Range {
int high;
int low;
Range({required this.low, required this.high}) {
assert(low <= high);
}
bool isWithin(int val) {
return low <= val && val <= high;
}
}
List<CardProvider> providers = [
CardProvider(
id: CardProviderID.americanExpress,
2023-11-14 09:58:49 -07:00
cardLength: 15,
cvcLength: 4,
innValidNums: [34, 37],
2023-11-14 09:58:49 -07:00
),
CardProvider(
id: CardProviderID.dinersClub,
2023-11-14 09:58:49 -07:00
cardLength: 16,
cvcLength: 3,
innValidNums: [30, 36, 38, 39],
2023-11-14 09:58:49 -07:00
),
CardProvider(
id: CardProviderID.discoverCard,
2023-11-14 09:58:49 -07:00
cardLength: 16,
cvcLength: 3,
innValidNums: [60, 65],
innValidRanges: [Range(low: 644, high: 649)],
2023-11-14 09:58:49 -07:00
),
CardProvider(
id: CardProviderID.jcb,
2023-11-14 09:58:49 -07:00
cardLength: 16,
cvcLength: 3,
innValidNums: [35],
2023-11-14 09:58:49 -07:00
),
CardProvider(
id: CardProviderID.mastercard,
2023-11-14 09:58:49 -07:00
cardLength: 16,
cvcLength: 3,
innValidRanges: [Range(low: 22, high: 27), Range(low: 51, high: 55)],
2023-11-14 09:58:49 -07:00
),
CardProvider(
id: CardProviderID.visa,
2023-11-14 09:58:49 -07:00
cardLength: 16,
cvcLength: 3,
innValidNums: [4],
2023-11-14 09:58:49 -07:00
)
];
// https://en.wikipedia.org/wiki/Luhn_algorithm
// The Luhn algorithm is used in industry to check
// for valid credit / debit card numbers
//
// The algorithm adds together all the numbers, every
// other number is doubled, then the sum is checked to
// see if it is a multiple of 10.
bool luhnAlgorithmCheck(List<int> digits) {
int sum = 0;
bool isSecond = false;
for (int i = digits.length - 1; i >= 0; i--) {
int d = digits[i];
if (isSecond) {
d *= 2;
if (d > 9) {
d -= 9;
}
}
sum += d;
isSecond = !isSecond;
}
return (sum % 10) == 0;
}