vivek bhoraniya
vivek bhoraniya

Reputation: 1535

Validate credit card number

I am taking credit card number from user as input. I want to allow 16 numbers and want to format space after each 4 number. For that I have done following.

-(void)cardNumberValidation:(id)sender{
    UITextField *temp=sender;

    if ([temp.text length]>19) {
        txtCard.text= [temp.text substringToIndex:[temp.text length] - 1];
    }
    if ([temp.text length]==4) {
        txtCard.text=[NSString stringWithFormat:@"%@ ",temp.text];

        }
    if ([temp.text length]==9) {
        txtCard.text=[NSString stringWithFormat:@"%@ ",temp.text];
        }
    if ([temp.text length]==14) {
        NSString *lastChar = [txtCard.text substringFromIndex:[txtCard.text length] - 1];
        txtCard.text=[NSString stringWithFormat:@"%@ ",temp.text];
    }
}

But now when user will remove character after entering all then if it will reach to 15 character and then user will remove one more character then this code will add space after it and will not allow to remove it further.

Can anyone give me suggestion on this or any better way to do it.

Upvotes: 4

Views: 21344

Answers (9)

Igor Ryazancev
Igor Ryazancev

Reputation: 11

converted code for swift 3+ (Eduardo Oliveros response)

enum CardType: String {
    case Unknown, Amex, Visa, MasterCard, Diners, Discover, JCB, Elo, Hipercard, UnionPay
    
    static let allCards = [Amex, Visa, MasterCard, Diners, Discover, JCB, Elo, Hipercard, UnionPay]
    
    var regex : String {
        switch self {
        case .Amex:
            return "^3[47][0-9]{5,}$"
        case .Visa:
            return "^4[0-9]{6,}([0-9]{3})?$"
        case .MasterCard:
            return "^(5[1-5][0-9]{4}|677189)[0-9]{5,}$"
        case .Diners:
            return "^3(?:0[0-5]|[68][0-9])[0-9]{4,}$"
        case .Discover:
            return "^6(?:011|5[0-9]{2})[0-9]{3,}$"
        case .JCB:
            return "^(?:2131|1800|35[0-9]{3})[0-9]{3,}$"
        case .UnionPay:
            return "^(62|88)[0-9]{5,}$"
        case .Hipercard:
            return "^(606282|3841)[0-9]{5,}$"
        case .Elo:
            return "^((((636368)|(438935)|(504175)|(451416)|(636297))[0-9]{0,10})|((5067)|(4576)|(4011))[0-9]{0,12})$"
        default:
            return ""
        }
    }
}
extension UITextField {
    
    func validateCreditCardFormat()-> (type: CardType, valid: Bool) {
        // Get only numbers from the input string
        var input = self.text!
        let numberOnly = input.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
        
        var type: CardType = .Unknown
        var formatted = ""
        var valid = false
        
        // detect card type
        for card in CardType.allCards {
            if (matchesRegex(regex: card.regex, text: numberOnly)) {
                type = card
                break
            }
        }
        
        // check validity
        valid = luhnCheck(number: numberOnly)
        
        // format
        var formatted4 = ""
        for character in numberOnly {
            if formatted4.count == 4 {
                formatted += formatted4 + " "
                formatted4 = ""
            }
            formatted4.append(character)
        }
        
        formatted += formatted4 // the rest
        
        // return the tuple
        return (type, valid)
    }
    
    func matchesRegex(regex: String!, text: String!) -> Bool {
        do {
            let regex = try NSRegularExpression(pattern: regex, options: [.caseInsensitive])
            let nsString = text as NSString
            let match = regex.firstMatch(in: text, options: [], range: NSMakeRange(0, nsString.length))
            return (match != nil)
        } catch {
            return false
        }
    }
    
    func luhnCheck(number: String) -> Bool {
        var sum = 0
        let digitStrings = number.reversed().map { String($0) }
        
        for tuple in digitStrings.enumerated() {
            guard let digit = Int(tuple.element) else { return false }
            let odd = tuple.0 % 2 == 1
            
            switch (odd, digit) {
            case (true, 9):
                sum += 9
            case (true, 0...8):
                sum += (digit * 2) % 9
            default:
                sum += digit
            }
        }
        
        return sum % 10 == 0
    }
}

Upvotes: 0

Eduardo Oliveros
Eduardo Oliveros

Reputation: 857

In Swift

enum CardType: String {
    case Unknown, Amex, Visa, MasterCard, Diners, Discover, JCB, Elo, Hipercard, UnionPay

    static let allCards = [Amex, Visa, MasterCard, Diners, Discover, JCB, Elo, Hipercard, UnionPay]

    var regex : String {
        switch self {
        case .Amex:
           return "^3[47][0-9]{5,}$"
        case .Visa:
           return "^4[0-9]{6,}([0-9]{3})?$"
        case .MasterCard:
           return "^(5[1-5][0-9]{4}|677189)[0-9]{5,}$"
        case .Diners:
           return "^3(?:0[0-5]|[68][0-9])[0-9]{4,}$"
        case .Discover:
           return "^6(?:011|5[0-9]{2})[0-9]{3,}$"
        case .JCB:
           return "^(?:2131|1800|35[0-9]{3})[0-9]{3,}$"
        case .UnionPay:
           return "^(62|88)[0-9]{5,}$"
        case .Hipercard:
           return "^(606282|3841)[0-9]{5,}$"
        case .Elo:
           return "^((((636368)|(438935)|(504175)|(451416)|(636297))[0-9]{0,10})|((5067)|(4576)|(4011))[0-9]{0,12})$"
        default:
           return ""
        }
    }
}
extension UITextField{ 

func validateCreditCardFormat()-> (type: CardType, valid: Bool) {
        // Get only numbers from the input string
        var input = self.text!
        let numberOnly = input.stringByReplacingOccurrencesOfString("[^0-9]", withString: "", options: .RegularExpressionSearch)

    var type: CardType = .Unknown
    var formatted = ""
    var valid = false

    // detect card type
    for card in CardType.allCards {
        if (matchesRegex(card.regex, text: numberOnly)) {
            type = card
            break
        }
    }

    // check validity
    valid = luhnCheck(numberOnly)

    // format
    var formatted4 = ""
    for character in numberOnly.characters {
        if formatted4.characters.count == 4 {
            formatted += formatted4 + " "
            formatted4 = ""
        }
        formatted4.append(character)
    }

    formatted += formatted4 // the rest

    // return the tuple
    return (type, valid)
}

func matchesRegex(regex: String!, text: String!) -> Bool {
    do {
        let regex = try NSRegularExpression(pattern: regex, options: [.CaseInsensitive])
        let nsString = text as NSString
        let match = regex.firstMatchInString(text, options: [], range: NSMakeRange(0, nsString.length))
        return (match != nil)
    } catch {
        return false
    }
}

func luhnCheck(number: String) -> Bool {
    var sum = 0
    let digitStrings = number.characters.reverse().map { String($0) }

    for tuple in digitStrings.enumerate() {
        guard let digit = Int(tuple.element) else { return false }
        let odd = tuple.index % 2 == 1

        switch (odd, digit) {
        case (true, 9):
            sum += 9
        case (true, 0...8):
            sum += (digit * 2) % 9
        default:
            sum += digit
        }
    }

    return sum % 10 == 0
}
}

form more go to http://kalapun.com/posts/card-checking-in-swift/

Upvotes: 17

zaph
zaph

Reputation: 112857

Don't change the user's entered text, it will just cause confusion. Don't cause the user to think: WTF. The user entered the number in the way he understood, honor that as much as possible.

Just sanitize what the user has entered. Generally just remove all leading, training and interspersed space characters, possibly any non-numeric characters. Then ensure the entered text is all numeric and of the correct length.

Keep in mind that the number can have a length of 13 to 19 digits, American Express is 15 digits. See: Bank card number

Consider the code:

if ([temp.text length]>19) {
    txtCard.text= [temp.text substringToIndex:[temp.text length] - 1];
}

If the user entered an extra space character between groups the last digit will be deleted. It is all to easy to come up with such a scheme will avoid all possible pitfalls.
Example: "1234 4567 9012 3456" would be truncated to "1234 4567 9012 345".

Extra, Method to verify the check digit:

+ (BOOL)isValidCheckDigitForCardNumberString:(NSString *)cardNumberString {
    int checkSum = 0;
    uint8_t *cardDigitArray = (uint8_t *)[cardNumberString dataUsingEncoding:NSUTF8StringEncoding].bytes;
    int digitsCount = (int)cardNumberString.length;
    BOOL odd = cardNumberString.length % 2;

    for (int digitIndex=0; digitIndex<digitsCount; digitIndex++) {
        uint8_t cardDigit = cardDigitArray[digitIndex] - '0';
        if (digitIndex % 2 == odd) {
            cardDigit = cardDigit * 2;
            cardDigit = cardDigit / 10 + cardDigit % 10;
        }
        checkSum += cardDigit;
    }

    return (checkSum % 10 == 0);
}

BOOL checkDigitValid = [TestClass isValidCheckDigitForCardNumberString:@"371238839571772"];
NSLog(@"check digit valid: %@", checkDigitMatch ? @"yes" : @"no");

Output:

check digit valid: yes

Upvotes: 5

Tesan3089
Tesan3089

Reputation: 422

Solution in Swift:

let z = 4, intervalString = " "

func canInsert(atLocation y:Int) -> Bool { return ((1 + y)%(z + 1) == 0) ? true : false }

func canRemove(atLocation y:Int) -> Bool { return (y != 0) ? (y%(z + 1) == 0) : false }

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

        let nsText = textField.text! as NSString

        if range.location == 19 { return false }

        if range.length == 0 && canInsert(atLocation: range.location) {
            textField.text! = textField.text! + intervalString + string
            return false
        }

        if range.length == 1 && canRemove(atLocation: range.location) {
            textField.text! = nsText.stringByReplacingCharactersInRange(NSMakeRange(range.location-1, 2), withString: "")
            return false
        }

        return true
    }

Upvotes: 1

Chetan Purohit
Chetan Purohit

Reputation: 392

You can do this by using following:

class PaymentViewController: UIViewController, UITextFieldDelegate {

@IBOutlet weak var textFieldCardNumber: UITextField!

 //for keeping track of cursor in text field for setting limit by Chetan
var cardNumberCursorPreviousPosition = 0


//MARK: - LifeCycle Methods
override func viewDidLoad() {
    super.viewDidLoad()

    self.textFieldCardNumber.delegate = self

    //for applying did change event on text fields
    self.textFieldCardNumber.addTarget(self, action: "textFieldDidChange:", forControlEvents: UIControlEvents.EditingChanged)

}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


func textFieldDidChange(textField: UITextField) {

    //logic for adding hyphen after each 4 digits
    if textField == self.textFieldCardNumber {

        if count(self.textFieldCardNumber.text) <= 7 {

            var cardType: String = cardValidator.checkCardType(self.textFieldCardNumber.text)
            println("Card Type : \(cardType)")

        }

        //for applying hyphen
        if (count(textFieldCardNumber.text) == 4  && cardNumberCursorPreviousPosition == 3) || (count(textFieldCardNumber.text) == 9 && cardNumberCursorPreviousPosition == 8) ||
            (count(textFieldCardNumber.text) == 14  && cardNumberCursorPreviousPosition == 13) {

            textFieldCardNumber.text = "\(textFieldCardNumber.text)-"

        }

        //for removing hyphen and its preceding character/number
        if (count(textFieldCardNumber.text) == 4 && cardNumberCursorPreviousPosition == 5) ||
            (count(textFieldCardNumber.text) == 9 && cardNumberCursorPreviousPosition == 10) ||
            (count(textFieldCardNumber.text) == 14 && cardNumberCursorPreviousPosition == 15) {

            textFieldCardNumber.text = textFieldCardNumber.text.substringToIndex(advance(textFieldCardNumber.text.endIndex, -1))

        }

         cardNumberCursorPreviousPosition = count(textFieldCardNumber.text)            

    }


}

In the above code I have given hyphen. You can replace it with space.

Upvotes: 0

Pawan Sharma
Pawan Sharma

Reputation: 3334

You should use payment kit a very powerful and easy to use API for such tasks.

Get Payment kit from GitHub

enter image description here

Upvotes: -5

David Berry
David Berry

Reputation: 41226

If you want to maintain your current approach, I'd suggest stripping all the spaces out and then reinserting them at the right places, something like:

-(void)cardNumberValidation:(id)sender
{
    NSString*   text = [sender text];

    // Strip out all spaces
    text = [text stringByReplacingOccurrencesOfString:@" " withString:@""];

    // Truncate to 16 characters
    if(text.length)
        text = [text substringToIndex:16];

    // Insert spaces
    if(text.length > 12)
        text = [text stringByReplacingCharactersInRange:NSMakeRange(12, 0) withString:@" "];
    if(text.length > 8)
        text = [text stringByReplacingCharactersInRange:NSMakeRange(8, 0) withString:@" "];
    if(text.length > 4)
        text = [text stringByReplacingCharactersInRange:NSMakeRange(4, 0) withString:@" "];

    [sender setText:text];
}

That said, the idea of changing the users text on them can be confusing to the user, and this approach is very dependent on only accepting VISA and/or MasterCard as other card issuers use different formats.

Upvotes: 1

Ruslanas Kudriavcevas
Ruslanas Kudriavcevas

Reputation: 89

Here is my quick solution:

#define kLENGTH 4

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    if (string.length > 0) {
        NSUInteger length = textField.text.length;
        int cntr = (int)((length - (length/kLENGTH)) / kLENGTH);
        if (!(((length + 1) % kLENGTH) - cntr)) {
            NSString *str = [textField.text stringByAppendingString:[NSString stringWithFormat:@"%@ ", string]];
            textField.text = str;
            return NO;
        }
    } else {
        if ([textField.text hasSuffix:@" "]) {
            textField.text = [textField.text substringToIndex:textField.text.length - 2];
            return NO;
        }
    }
    return YES;
}

Upvotes: 1

Pawan Rai
Pawan Rai

Reputation: 3444

i am using this one in one of my app for credit card like format

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init] ; 
    if([string length]==0)
    {
        [formatter setGroupingSeparator:@"-"];
        [formatter setGroupingSize:4];
        [formatter setUsesGroupingSeparator:YES];
        [formatter setSecondaryGroupingSize:2];
        NSString *num = textField.text ;
        num= [num stringByReplacingOccurrencesOfString:@"-" withString:@""];
        NSString *str = [formatter stringFromNumber:[NSNumber numberWithDouble:[num doubleValue]]];
        [formatter release];
        textField.text=str;
        NSLog(@"%@",str);
        return YES;
    }
    else {
        [formatter setGroupingSeparator:@"-"];
        [formatter setGroupingSize:2];
        [formatter setUsesGroupingSeparator:YES];
        [formatter setSecondaryGroupingSize:2];
        NSString *num = textField.text ;
        if(![num isEqualToString:@""])
        {
            num= [num stringByReplacingOccurrencesOfString:@"-" withString:@""];
            NSString *str = [formatter stringFromNumber:[NSNumber numberWithDouble:[num doubleValue]]];
            [formatter release];
            textField.text=str;
        }

        //NSLog(@"%@",str);
        return YES;
    }

    //[formatter setLenient:YES];

}

Upvotes: 2

Related Questions