Last active
July 11, 2019 09:46
-
-
Save lalkrishna/0b9a551f2bbc08944c73c4709016802a to your computer and use it in GitHub Desktop.
DebitCard - Auto detect card type. Supported Cards: American Express, Discover, MasterCard, Visa Card
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LKDebitCard.m | |
// DebitCard | |
// | |
// Created by LK on 21/12/17. | |
// Copyright © 2017 LK. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
@interface NSString (Luhn_Private) | |
- (NSString *) formattedStringForProcessing; | |
@end | |
typedef NS_ENUM(NSInteger, PaymentMethod) { | |
PaymentMethodNotSet, | |
PaymentMethodPaypal, | |
PaymentMethodDebitCard, | |
}; | |
typedef NS_ENUM(NSUInteger, CardType) { | |
CardTypeInvalid, | |
CardTypeUnsupported, | |
CardTypeAmericanExpress, | |
CardTypeDiscover, | |
CardTypeMasterCard, | |
CardTypeVisa, | |
}; | |
static NSUInteger const MaxZipLength = 5; | |
@interface LKDebitCard : NSObject | |
@property (assign, nonatomic) CardType type; | |
@property (strong, nonatomic) NSString *strNumber; | |
@property (strong, nonatomic) NSString *strExpireOn; | |
@property (strong, nonatomic) NSString *strCVV; | |
@property (strong, nonatomic) NSString *strZip; | |
@property (assign, nonatomic, readonly, getter=isValid) BOOL valid; | |
- (UIImage *)icon; | |
- (UIImage *)iconFilled:(BOOL)filled; | |
- (void)findCardType; | |
- (BOOL)minLengthReached; | |
- (BOOL)isMaxed; | |
- (BOOL)appendCardNumber:(NSString *)number; | |
- (BOOL)deleteLastCardNumber; | |
- (BOOL)appendExpiry:(NSString *)number; | |
- (BOOL)deleteExpiryLastDigit; | |
- (BOOL)appendCVV:(NSString *)number; | |
- (BOOL)deleteLastCVVDigit; | |
- (BOOL)appendZIP:(NSString *)number; | |
- (BOOL)deleteLastZIPDigit; | |
- (NSUInteger)maxLengthCVV; | |
- (BOOL)isValidCardNumber; | |
- (BOOL)isValidExpiry; | |
- (BOOL)isValidCVV; | |
- (BOOL)isValidZip; | |
- (BOOL)isValidCardDetails; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LKDebitCard.m | |
// DebitCard | |
// | |
// Created by LK on 21/12/17. | |
// Copyright © 2017 LK. All rights reserved. | |
// | |
#import "LKDebitCard.h" | |
@implementation NSString (Luhn_Private) | |
- (NSString *) formattedStringForProcessing { | |
NSCharacterSet *illegalCharacters = [[NSCharacterSet decimalDigitCharacterSet] invertedSet]; | |
NSArray *components = [self componentsSeparatedByCharactersInSet:illegalCharacters]; | |
return [components componentsJoinedByString:@""]; | |
} | |
@end | |
@implementation LKDebitCard | |
@synthesize valid = _valid; | |
- (instancetype)init { | |
self = [super init]; | |
if (self) { | |
self.strNumber = @""; | |
} | |
return self; | |
} | |
- (instancetype)copyWithZone:(NSZone *)zone { | |
LKDebitCard *card = [LKDebitCard new]; | |
if (card) { | |
card.type = self.type; | |
card.strNumber = [self.strNumber copyWithZone:zone]; | |
self.strExpireOn = [self.strExpireOn copyWithZone:zone]; | |
self.strCVV = [self.strCVV copyWithZone:zone]; | |
self.strZip = [self.strZip copyWithZone:zone]; | |
} | |
return card; | |
} | |
#pragma mark - Getter | |
- (BOOL)isValid { | |
return [self validateString:self.strNumber]; | |
} | |
#pragma mark - Valid | |
- (BOOL)isValidCardNumber { | |
return !(!self.isValid || self.type == CardTypeInvalid || self.type == CardTypeUnsupported); | |
} | |
- (BOOL)isValidExpiry { | |
return self.strExpireOn.length >= 5; | |
} | |
- (BOOL)isValidCVV { | |
return self.strCVV.length == [self maxLengthCVV]; | |
} | |
- (BOOL)isValidZip { | |
return self.strZip.length >= MaxZipLength; | |
} | |
- (BOOL)isValidCardDetails { | |
return [self isValidCardNumber] && [self isValidExpiry] && [self isValidCVV] && [self isValidZip]; | |
} | |
#pragma mark - | |
#pragma mark - Public Methods | |
- (UIImage *)icon { | |
return [self iconFilled:NO]; | |
} | |
- (UIImage *)iconFilled:(BOOL)filled { | |
if (self.type == CardTypeInvalid || self.type == CardTypeUnsupported) { | |
return nil; | |
} | |
NSString *imageName; | |
switch (_type) { | |
case CardTypeAmericanExpress: | |
imageName = @"amex"; | |
break; | |
case CardTypeDiscover: | |
imageName = @"discover"; | |
break; | |
case CardTypeMasterCard: | |
imageName = @"mastercard"; | |
break; | |
case CardTypeVisa: | |
imageName = @"visa"; | |
break; | |
default: | |
break; | |
} | |
if (filled) { | |
imageName = [NSString stringWithFormat:@"%@Fill", imageName]; | |
} | |
UIImage *icon = [UIImage imageNamed:imageName]; | |
return icon; | |
} | |
- (void)findCardType { | |
CardType type = [self typeFromString:self.strNumber]; | |
self.type = type; | |
} | |
#pragma mark - Expiry | |
- (BOOL)appendExpiry:(NSString *)number { | |
NSString *expiry = self.strExpireOn ?: @""; | |
NSMutableString *mutableString = [NSMutableString stringWithString:expiry]; | |
if (mutableString.length == 0 && [number integerValue] > 1) { | |
[mutableString appendString:@"0"]; | |
} | |
if (mutableString.length == 2) { | |
[mutableString appendString:@"/"]; | |
} | |
[mutableString appendString:number]; | |
if (mutableString.length == 2 && [mutableString integerValue] > 12) { | |
return false; | |
} | |
if (mutableString.length > 5) { | |
return false; | |
} | |
self.strExpireOn = mutableString; | |
return true; | |
} | |
- (BOOL)deleteExpiryLastDigit { | |
if (self.strExpireOn.length < 1) { | |
// self.strExpireOn = nil; | |
return NO; | |
} | |
if (self.strExpireOn.length > 1) { | |
unichar secondLastChar = [self.strExpireOn characterAtIndex:[self.strExpireOn length] - 2]; | |
if (secondLastChar == '/') { | |
self.strExpireOn = [self.strExpireOn substringToIndex:[self.strExpireOn length] - 2]; | |
return YES; | |
} | |
} | |
self.strExpireOn = [self.strExpireOn substringToIndex:[self.strExpireOn length] - 1]; | |
return YES; | |
} | |
#pragma mark - CVV | |
- (BOOL)appendCVV:(NSString *)number { | |
NSString *cvv = self.strCVV ?: @""; | |
cvv = [NSString stringWithFormat:@"%@%@", cvv, number]; | |
if (cvv.length > [self maxLengthCVV]) { | |
return false; | |
} | |
self.strCVV = cvv; | |
return true; | |
} | |
- (BOOL)deleteLastCVVDigit { | |
self.strCVV = [self deleteLastDigit:self.strCVV]; | |
return true; | |
} | |
- (NSUInteger)maxLengthCVV { | |
NSUInteger maxLength = (self.type == CardTypeAmericanExpress) ? 4 : 3; | |
return maxLength; | |
} | |
#pragma mark - ZIP | |
- (BOOL)appendZIP:(NSString *)number { | |
NSString *zip = self.strZip ?: @""; | |
zip = [NSString stringWithFormat:@"%@%@", zip, number]; | |
if (zip.length > MaxZipLength) { | |
return false; | |
} | |
self.strZip = zip; | |
return true; | |
} | |
- (BOOL)deleteLastZIPDigit { | |
self.strZip = [self deleteLastDigit:self.strZip]; | |
return true; | |
} | |
#pragma mark - Common | |
- (NSString *)deleteLastDigit:(NSString *)string { | |
if (string.length < 1) { | |
return @""; | |
} | |
string = [string substringToIndex:[string length] - 1]; | |
return string; | |
} | |
#pragma mark - Card Number | |
- (BOOL)appendCardNumber:(NSString *)number { | |
if (!number) { | |
return NO; | |
} | |
NSString *text = self.strNumber; | |
NSMutableString *mutableString = [NSMutableString stringWithString:text]; | |
if (text.length < 4) { | |
[mutableString appendString:number]; | |
} | |
if (mutableString.length == 4 ) { | |
// Time to detect card type | |
CardType type = [self typeFromString:mutableString]; | |
NSLog(@"Card Type Detected: %zd", type); | |
self.type = type; | |
} | |
NSUInteger maxLength = [self maximumCardLength]; | |
if (mutableString.length >= maxLength) { | |
return NO; | |
} | |
if (text.length >= 4) { | |
if (self.type == CardTypeAmericanExpress) { | |
if (text.length == 4 || text.length == 11) { | |
[mutableString appendString:@" "]; | |
} | |
} else { | |
if (text.length == 4 ||text.length == 9 || text.length == 14 || text.length == 19 ) { | |
[mutableString appendString:@" "]; | |
} | |
} | |
[mutableString appendString:number]; | |
} | |
self.strNumber = mutableString; | |
return YES; | |
} | |
- (BOOL)deleteLastCardNumber { | |
if (self.strNumber.length < 1) { | |
return NO; | |
} | |
if (self.strNumber.length > 1) { | |
unichar secondLastChar = [self.strNumber characterAtIndex:[self.strNumber length] - 2]; | |
if (secondLastChar == ' ') { | |
self.strNumber = [self.strNumber substringToIndex:[self.strNumber length] - 2]; | |
return YES; | |
} | |
} | |
self.strNumber = [self.strNumber substringToIndex:[self.strNumber length] - 1]; | |
return YES; | |
} | |
- (BOOL)isMaxed { | |
NSUInteger maxLength = [self maximumCardLength]; | |
if (self.strNumber.length >= maxLength) { | |
return YES; | |
} | |
return NO; | |
} | |
- (NSUInteger)maximumCardLength { | |
return [self maxLengthForCardType:self.type]; | |
} | |
- (NSUInteger) maxLengthForCardType:(CardType)type { | |
switch (type) { | |
case CardTypeAmericanExpress: | |
return 15 + 2; | |
break; | |
case CardTypeDiscover: | |
return 19 + 4; | |
break; | |
case CardTypeMasterCard: | |
return 16 + 3; | |
break; | |
case CardTypeVisa: | |
return 19 + 4; | |
break; | |
default: | |
break; | |
} | |
return 16 + 3; | |
} | |
- (BOOL)minLengthReached { | |
NSUInteger minLength = [self minLengthForCardType:self.type]; | |
if (self.strNumber.length >= minLength) { | |
return YES; | |
} | |
return NO; | |
} | |
- (NSUInteger) minLengthForCardType:(CardType)type { | |
switch (type) { | |
case CardTypeAmericanExpress: | |
return 15 + 2; | |
break; | |
case CardTypeDiscover: | |
return 16 + 3; | |
break; | |
case CardTypeMasterCard: | |
return 16 + 3; | |
break; | |
case CardTypeVisa: | |
return 13 + 3; | |
break; | |
default: | |
break; | |
} | |
return 13 + 3; | |
} | |
#pragma mark - Private Methods | |
- (BOOL)validateString:(NSString *)string { | |
NSString *formattedString = [string formattedStringForProcessing]; | |
if (formattedString == nil || formattedString.length < 9 || ![self minLengthReached]) { | |
return NO; | |
} | |
NSMutableString *reversedString = [NSMutableString stringWithCapacity:[formattedString length]]; | |
[formattedString enumerateSubstringsInRange:NSMakeRange(0, [formattedString length]) options:(NSStringEnumerationReverse |NSStringEnumerationByComposedCharacterSequences) usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { | |
[reversedString appendString:substring]; | |
}]; | |
NSUInteger oddSum = 0, evenSum = 0; | |
for (NSUInteger i = 0; i < [reversedString length]; i++) { | |
NSInteger digit = [[NSString stringWithFormat:@"%C", [reversedString characterAtIndex:i]] integerValue]; | |
if (i % 2 == 0) { | |
evenSum += digit; | |
} | |
else { | |
oddSum += digit / 5 + (2 * digit) % 10; | |
} | |
} | |
return (oddSum + evenSum) % 10 == 0; | |
} | |
- (CardType) typeFromString:(NSString *) string { | |
NSString *formattedString = [string formattedStringForProcessing]; | |
if (formattedString == nil || formattedString.length < 4) { | |
return CardTypeInvalid; | |
} | |
NSArray<NSNumber *> *enums = @[ @(CardTypeAmericanExpress), @(CardTypeDiscover), @(CardTypeMasterCard), @(CardTypeVisa) ]; | |
__block CardType type = CardTypeInvalid; | |
[enums enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { | |
CardType _type = [obj integerValue]; | |
NSPredicate *predicate = [LKDebitCard predicateForType:_type]; | |
BOOL isCurrentType = [predicate evaluateWithObject:formattedString]; | |
if (isCurrentType) { | |
type = _type; | |
*stop = YES; | |
} | |
}]; | |
return type; | |
} | |
+ (NSPredicate *) predicateForType:(CardType) type { | |
if (type == CardTypeInvalid || type == CardTypeUnsupported) { | |
return nil; | |
} | |
NSString *regex = nil; | |
switch (type) { | |
case CardTypeAmericanExpress: | |
regex = @"^3[47][0-9]{0,}$"; //^3[47][0-9]{5,}$ | |
break; | |
case CardTypeDiscover: | |
regex = @"^(6011|65|64[4-9]|62212[6-9]|6221[3-9]|622[2-8]|6229[01]|62292[0-5])[0-9]{0,}$"; //^6(?:011|5[0-9]{2})[0-9]{3,}$ | |
break; | |
case CardTypeMasterCard: | |
regex = @"^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[01]|2720)[0-9]{0,}$"; //^5[1-5][0-9]{5,}|222[1-9][0-9]{3,}|22[3-9][0-9]{4,}|2[3-6][0-9]{5,}|27[01][0-9]{4,}|2720[0-9]{3,}$ | |
break; | |
case CardTypeVisa: | |
regex = @"^4[0-9]{0,}$";//^4[0-9]{6,}$ | |
break; | |
default: | |
break; | |
} | |
return [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment