Skip to content

Instantly share code, notes, and snippets.

@lalkrishna
Last active July 11, 2019 09:46
Show Gist options
  • Save lalkrishna/0b9a551f2bbc08944c73c4709016802a to your computer and use it in GitHub Desktop.
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
//
// 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
//
// 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