-
Notifications
You must be signed in to change notification settings - Fork 307
/
card.dart
411 lines (343 loc) · 11.1 KB
/
card.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
import 'package:flutter_paystack/src/common/card_utils.dart';
import 'package:flutter_paystack/src/common/string_utils.dart';
import 'package:meta/meta.dart';
/// The class for the Payment Card model. Has utility methods for validating
/// the card.
class PaymentCard {
// List of supported card types
final List<CardType> cardTypes = [
_Visa(),
_MasterCard(),
_AmericanExpress(),
_DinersClub(),
_Discover(),
_Jcb(),
_Verve()
];
/// Name on card
String name;
/// Card number
String _number;
/// Card CVV or CVC
String _cvc;
/// Expiry month
int expiryMonth = 0;
/// Expiry year
int expiryYear = 0;
/// Bank Address line 1
String addressLine1;
/// Bank Address line 2
String addressLine2;
/// Bank Address line 3
String addressLine3;
/// Bank Address line 4
String addressLine4;
/// Postal code of the bank address
String addressPostalCode;
/// Country of the bank
String addressCountry;
String country;
/// Type of card
String _type;
String _last4Digits;
set type(String value) => _type = value;
String get number => _number;
String get last4Digits => _last4Digits;
String get type {
// If type is empty and the number isn't empty
if (StringUtils.isEmpty(_type) && !StringUtils.isEmpty(number)) {
for (var cardType in cardTypes) {
if (cardType.hasFullMatch(number)) {
return cardType.toString();
}
}
return CardType.unknown;
}
return _type;
}
// Get the card type by matching the starting characters against the Issuer
// Identification Number (IIN)
String getTypeForIIN(String cardNumber) {
// If type is empty and the number isn't empty
if (!StringUtils.isEmpty(cardNumber)) {
for (var cardType in cardTypes) {
if (cardType.hasStartingMatch(cardNumber)) {
return cardType.toString();
}
}
return CardType.unknown;
}
return CardType.unknown;
}
set number(String value) {
_number = CardUtils.getCleanedNumber(value);
if (number.length == 4) {
_last4Digits = number;
} else if (number.length > 4) {
_last4Digits = number.substring(number.length - 4);
} else {
// whatever is appropriate in this case
_last4Digits = number;
}
}
nullifyNumber() {
_number = null;
}
String get cvc => _cvc;
set cvc(String value) {
_cvc = CardUtils.getCleanedNumber(value);
}
PaymentCard(
{@required String number,
@required String cvc,
@required this.expiryMonth,
@required this.expiryYear,
String name,
String addressLine1,
String addressLine2,
String addressLine3,
String addressLine4,
String addressPostCode,
String addressCountry,
String country}) {
this.number = number;
this.cvc = cvc;
this.name = StringUtils.nullify(name);
this.addressLine1 = StringUtils.nullify(addressLine1);
this.addressLine2 = StringUtils.nullify(addressLine2);
this.addressLine3 = StringUtils.nullify(addressLine3);
this.addressLine4 = StringUtils.nullify(addressLine4);
this.addressCountry = StringUtils.nullify(addressCountry);
this.addressPostalCode = StringUtils.nullify(addressPostalCode);
this.country = StringUtils.nullify(country);
this.type = type;
}
PaymentCard.empty() {
this.expiryYear = 0;
this.expiryMonth = 0;
this._number = null;
this.cvc = null;
}
/// Validates the CVC or CVV of the card
/// Returns true if the cvc is valid
bool isValid() {
return cvc != null &&
number != null &&
expiryMonth != null &&
expiryYear != null &&
validNumber(null) &&
CardUtils.validExpiryDate(expiryMonth, expiryYear) &&
validCVC(null);
}
/// Validates the CVC or CVV of a card.
/// Returns true if CVC is valid and false otherwise
bool validCVC(String cardCvc) {
if (cardCvc == null) {
cardCvc = this.cvc;
}
if (cardCvc == null || cardCvc.trim().isEmpty) return false;
var cvcValue = cardCvc.trim();
bool validLength =
((_type == null && cvcValue.length >= 3 && cvcValue.length <= 4) ||
(CardType.americanExpress == _type && cvcValue.length == 4) ||
(CardType.americanExpress != _type && cvcValue.length == 3));
return !(!CardUtils.isWholeNumberPositive(cvcValue) || !validLength);
}
/// Validates the number of the card
/// Returns true if the number is valid. Returns false otherwise
bool validNumber(String cardNumber) {
if (cardNumber == null) {
cardNumber = this.number;
}
if (StringUtils.isEmpty(cardNumber)) return false;
// Remove all non digits
var formattedNumber =
cardNumber.trim().replaceAll(new RegExp(r'[^0-9]'), '');
// Verve card needs no other validation except it matched pattern
if (CardType.fullPatternVerve.hasMatch(formattedNumber)) {
return true;
}
//check if formattedNumber is empty or card isn't a whole positive number or isn't Luhn-valid
if (StringUtils.isEmpty(formattedNumber) ||
!CardUtils.isWholeNumberPositive(cardNumber) ||
!_isValidLuhnNumber(cardNumber)) return false;
// check type lengths
if (CardType.americanExpress == _type) {
return formattedNumber.length == CardType.maxLengthAmericanExpress;
} else if (CardType.dinersClub == _type) {
return formattedNumber.length == CardType.maxLengthDinersClub;
} else {
return formattedNumber.length == CardType.maxLengthNormal;
}
}
/// Validates the number against Luhn algorithm https://de.wikipedia.org/wiki/Luhn-Algorithmus#Java
/// [number] - number to validate
/// Returns true if the number is passes the verification.
bool _isValidLuhnNumber(String number) {
int sum = 0;
int length = number.trim().length;
for (var i = 0; i < length; i++) {
// get digits in reverse order
var source = number[length - i - 1];
// Check if character is digit before parsing it
if (!((number.codeUnitAt(i) ^ 0x30) <= 9)) {
return false;
}
int digit = int.parse(source);
// if it's odd, multiply by 2
if (i % 2 == 1) {
digit *= 2;
}
sum += digit > 9 ? (digit - 9) : digit;
}
return sum % 10 == 0;
}
@override
String toString() {
return 'PaymentCard{_cvc: $_cvc, expiryMonth: $expiryMonth, expiryYear: '
'$expiryYear, _type: $_type, _last4Digits: $_last4Digits , _number: '
'$_number}';
}
}
abstract class CardType {
// Card types
static const String visa = "Visa";
static const String masterCard = "MasterCard";
static const String americanExpress = "American Express";
static const String dinersClub = "Diners Club";
static const String discover = "Discover";
static const String jcb = "JCB";
static const String verve = "VERVE";
static const String unknown = "Unknown";
// Length for some cards
static final int maxLengthNormal = 16;
static final int maxLengthAmericanExpress = 15;
static final int maxLengthDinersClub = 14;
// Regular expressions to match complete numbers of the card
//source of these regex patterns http://stackoverflow.com/questions/72768/how-do-you-detect-credit-card-type-based-on-number
static final fullPatternVisa = RegExp(r'^4[0-9]{12}(?:[0-9]{3})?$');
static final fullPatternMasterCard = RegExp(
r'^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$');
static final fullPatternAmericanExpress = RegExp(r'^3[47][0-9]{13}$');
static final fullPatternDinersClub = RegExp(r'^3(?:0[0-5]|[68][0-9])'
r'[0-9]{11}$');
static final fullPatternDiscover = RegExp(r'^6(?:011|5[0-9]{2})[0-9]{12}$');
static final fullPatternJCB = RegExp(r'^(?:2131|1800|35[0-9]{3})'
r'[0-9]{11}$');
static final fullPatternVerve =
RegExp(r'^((506(0|1))|(507(8|9))|(6500))[0-9]{12,15}$');
// Regular expression to match starting characters (aka issuer
// identification number (IIN)) of the card
// Source https://en.wikipedia.org/wiki/Payment_card_number
static final startingPatternVisa = RegExp(r'[4]');
static final startingPatternMasterCard = RegExp(
r'((5[1-5])|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720))');
static final startingPatternAmericanExpress = RegExp(r'((34)|(37))');
static final startingPatternDinersClub =
RegExp(r'((30[0-5])|(3[89])|(36)|(3095))');
static final startingPatternJCB = RegExp(r'(352[89]|35[3-8][0-9])');
static final startingPatternVerve = RegExp(r'((506(0|1))|(507(8|9))|(6500))');
static final startingPatternDiscover = RegExp(r'((6[45])|(6011))');
bool hasFullMatch(String cardNumber);
bool hasStartingMatch(String cardNumber);
@override
String toString();
}
class _Visa extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternVisa.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternVisa);
}
@override
String toString() {
return CardType.visa;
}
}
class _MasterCard extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternMasterCard.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternMasterCard);
}
@override
String toString() {
return CardType.masterCard;
}
}
class _AmericanExpress extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternAmericanExpress.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternAmericanExpress);
}
@override
String toString() {
return CardType.americanExpress;
}
}
class _DinersClub extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternDinersClub.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternDinersClub);
}
@override
String toString() {
return CardType.dinersClub;
}
}
class _Discover extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternDiscover.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternDiscover);
}
@override
String toString() {
return CardType.discover;
}
}
class _Jcb extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternJCB.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternJCB);
}
@override
String toString() {
return CardType.jcb;
}
}
class _Verve extends CardType {
@override
bool hasFullMatch(String cardNumber) {
return CardType.fullPatternVerve.hasMatch(cardNumber);
}
@override
bool hasStartingMatch(String cardNumber) {
return cardNumber.startsWith(CardType.startingPatternVerve);
}
@override
String toString() {
return CardType.verve;
}
}