Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes STPPaymentCardTextField layout issues on small screens #1009

Merged
merged 10 commits into from
Aug 13, 2018
10 changes: 10 additions & 0 deletions Stripe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,10 @@
04FCFA191BD59A8C00297732 /* STPCategoryLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 04FCFA171BD59A8C00297732 /* STPCategoryLoader.h */; };
3617A51420FE5BBB001A9E6A /* NSLocale+STPSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = 3617A51220FE5BBB001A9E6A /* NSLocale+STPSwizzling.h */; };
3617A51520FE5BBB001A9E6A /* NSLocale+STPSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 3617A51320FE5BBB001A9E6A /* NSLocale+STPSwizzling.m */; };
3691EB712119111A008C49E1 /* STPCardValidator+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 3691EB6F2119111A008C49E1 /* STPCardValidator+Private.h */; };
3691EB722119111A008C49E1 /* STPCardValidator+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 3691EB702119111A008C49E1 /* STPCardValidator+Private.m */; };
3691EB74211A4F31008C49E1 /* STPShippingAddressViewControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 3691EB73211A4F31008C49E1 /* STPShippingAddressViewControllerTest.m */; };
36A734282121F8A700784615 /* STPCardValidator+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 3691EB702119111A008C49E1 /* STPCardValidator+Private.m */; };
8B013C891F1E784A00DD831B /* STPPaymentConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B013C881F1E784A00DD831B /* STPPaymentConfigurationTest.m */; };
8B39128220E2F99600098401 /* EPSSource.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B39128120E2F99600098401 /* EPSSource.json */; };
8B39128320E2F9A100098401 /* BancontactSource.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B39127F20E2F6A500098401 /* BancontactSource.json */; };
Expand Down Expand Up @@ -1047,6 +1050,8 @@
11C74B9B164043050071C2CA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
3617A51220FE5BBB001A9E6A /* NSLocale+STPSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSLocale+STPSwizzling.h"; sourceTree = "<group>"; };
3617A51320FE5BBB001A9E6A /* NSLocale+STPSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSLocale+STPSwizzling.m"; sourceTree = "<group>"; };
3691EB6F2119111A008C49E1 /* STPCardValidator+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPCardValidator+Private.h"; sourceTree = "<group>"; };
3691EB702119111A008C49E1 /* STPCardValidator+Private.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "STPCardValidator+Private.m"; sourceTree = "<group>"; };
3691EB73211A4F31008C49E1 /* STPShippingAddressViewControllerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPShippingAddressViewControllerTest.m; sourceTree = "<group>"; };
4A0D74F918F6106100966D7B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
7E0B1132203572FB00271AD3 /* fi */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = fi; path = Localizations/fi.lproj/Localizable.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1884,6 +1889,8 @@
F12829D91D7747E4008B10D6 /* STPBundleLocator.m */,
C1785F5A1EC60B5E00E9CFAC /* STPCardIOProxy.h */,
C1785F5B1EC60B5E00E9CFAC /* STPCardIOProxy.m */,
3691EB6F2119111A008C49E1 /* STPCardValidator+Private.h */,
3691EB702119111A008C49E1 /* STPCardValidator+Private.m */,
04FCFA171BD59A8C00297732 /* STPCategoryLoader.h */,
04633B0A1CD44F6C009D4FB5 /* STPCategoryLoader.m */,
0426B96C1CEADC98006AC8DD /* STPColorUtils.h */,
Expand Down Expand Up @@ -2357,6 +2364,7 @@
049952D21BCF13DD0088C703 /* STPAPIClient+Private.h in Headers */,
8B429AD81EF9D4B400F95F34 /* STPBankAccountParams+Private.h in Headers */,
049A3F911CC740FF00F57DE7 /* NSDecimalNumber+Stripe_Currency.h in Headers */,
3691EB712119111A008C49E1 /* STPCardValidator+Private.h in Headers */,
04633B071CD44F47009D4FB5 /* STPAPIClient+ApplePay.h in Headers */,
04BC29BD1CDD535700318357 /* STPSwitchTableViewCell.h in Headers */,
04CDB5121A5F30A700B854EE /* STPToken.h in Headers */,
Expand Down Expand Up @@ -2940,6 +2948,7 @@
04F94DA91D229F32004FC826 /* STPPaymentMethodTuple.m in Sources */,
C15608E01FE08F2E0032AE66 /* UIView+Stripe_SafeAreaBounds.m in Sources */,
F1A2F92F1EEB6A70006B0456 /* NSCharacterSet+Stripe.m in Sources */,
36A734282121F8A700784615 /* STPCardValidator+Private.m in Sources */,
F19491E51E60DD72001E1FC2 /* STPSourceSEPADebitDetails.m in Sources */,
C1785F5F1EC60B5E00E9CFAC /* STPCardIOProxy.m in Sources */,
0438EF311B7416BB00D506CC /* STPFormTextField.m in Sources */,
Expand Down Expand Up @@ -3110,6 +3119,7 @@
C158AB401E1EE98900348D01 /* STPSectionHeaderView.m in Sources */,
F148ABC81D5D334B0014FD92 /* STPLocalizationUtils.m in Sources */,
04A4C38B1C4F25F900B3B290 /* NSArray+Stripe.m in Sources */,
3691EB722119111A008C49E1 /* STPCardValidator+Private.m in Sources */,
F19491E41E60DD72001E1FC2 /* STPSourceSEPADebitDetails.m in Sources */,
04A4C38F1C4F25F900B3B290 /* UIViewController+Stripe_ParentViewController.m in Sources */,
049A3FAF1CC9AA9900F57DE7 /* STPAddressViewModel.m in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Stripe/NSString+Stripe.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- (NSString *)stp_safeSubstringToIndex:(NSUInteger)index;
- (NSString *)stp_safeSubstringFromIndex:(NSUInteger)index;
- (NSString *)stp_reversedString;
- (NSString *)stp_stringByRemovingSuffix:(NSString *)suffix;

@end

Expand Down
8 changes: 8 additions & 0 deletions Stripe/NSString+Stripe.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ - (NSString *)stp_reversedString {
return [mutableReversedString copy];
}

- (NSString *)stp_stringByRemovingSuffix:(NSString *)suffix {
if (suffix != nil && [self hasSuffix:suffix]) {
return [self stp_safeSubstringToIndex:self.length-suffix.length];
} else {
return [self copy];
}
}

@end

void linkNSStringCategory(void){}
21 changes: 21 additions & 0 deletions Stripe/STPCardValidator+Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// STPCardValidator+Private.h
// StripeiOS
//
// Created by Cameron Sabol on 8/6/18.
// Copyright © 2018 Stripe, Inc. All rights reserved.
//

#import "STPCardValidator.h"

NS_ASSUME_NONNULL_BEGIN

@interface STPCardValidator (Private)

+ (NSArray<NSNumber *> *)cardNumberFormatForBrand:(STPCardBrand)brand;

@end

NS_ASSUME_NONNULL_END

void linkSTPCardValidatorPrivateCategory(void);
31 changes: 31 additions & 0 deletions Stripe/STPCardValidator+Private.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// STPCardValidator+Private.m
// StripeiOS
//
// Created by Cameron Sabol on 8/6/18.
// Copyright © 2018 Stripe, Inc. All rights reserved.
//

#import "STPCardValidator+Private.h"

NS_ASSUME_NONNULL_BEGIN

@implementation STPCardValidator (Private)

+ (NSArray<NSNumber *> *)cardNumberFormatForBrand:(STPCardBrand)brand
{
switch (brand) {
case STPCardBrandAmex:
return @[@4, @6, @5];
case STPCardBrandDinersClub:
return @[@4, @6, @4];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fragmentLengthForCardBrand: returns 2 for Diners club. Should it do something different? Or should this end in 2?

The length of the final grouping of digits to use when formatting a card number for display.

Should fragmentLengthForCardBrand: call this method and just return the last element of the array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it should until I saw that they were different. I'm not sure on the source for the 2 fragment length. These values are from https://git.corp.stripe.com/stripe-internal/checkout/blob/d9430b90/manhattan/src/scripts/vendor/stripe-js-v3/src/lib/fields/format.js#L19

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web dashboard shows the last 4
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also do not know the source of the 2, I think it's (probably?) desirable to update the behavior of fragmentLengthForCardBrand:

default:
return @[@4, @4, @4, @4];
}
}

@end

NS_ASSUME_NONNULL_END

void linkSTPCardValidatorPrivateCategory(void){}
10 changes: 2 additions & 8 deletions Stripe/STPCardValidator.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "STPCardValidator.h"
#import "STPCardValidator+Private.h"

#import "STPBINRange.h"
#import "NSCharacterSet+Stripe.h"
Expand Down Expand Up @@ -241,14 +242,7 @@ + (NSInteger)maxLengthForCardBrand:(STPCardBrand)brand {
}

+ (NSInteger)fragmentLengthForCardBrand:(STPCardBrand)brand {
switch (brand) {
case STPCardBrandAmex:
return 5;
case STPCardBrandDinersClub:
return 2;
default:
return 4;
}
return [[[self cardNumberFormatForBrand:brand] lastObject] unsignedIntegerValue];
}

+ (BOOL)stringIsValidLuhn:(NSString *)number {
Expand Down
2 changes: 2 additions & 0 deletions Stripe/STPCategoryLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#import "PKPaymentAuthorizationViewController+Stripe_Blocks.h"
#import "STPAPIClient+ApplePay.h"
#import "STPAspects.h"
#import "STPCardValidator+Private.h"
#import "STPCustomer+SourceTuple.h"
#import "StripeError.h"
#import "UIBarButtonItem+Stripe.h"
Expand Down Expand Up @@ -54,6 +55,7 @@ + (void)loadCategories {
linkPKPaymentAuthorizationViewControllerBlocksCategory();
linkPKPaymentCategory();
linkSTPAPIClientApplePayCategory();
linkSTPCardValidatorPrivateCategory();
linkSTPCustomerSourceTupleCategory();
linkUIBarButtonItemCategory();
linkUIImageCategory();
Expand Down
27 changes: 14 additions & 13 deletions Stripe/STPFormTextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#import "NSString+Stripe.h"
#import "STPCardValidator.h"
#import "STPCardValidator+Private.h"
#import "STPDelegateProxy.h"
#import "STPPhoneNumberValidator.h"
#import "STPWeakStrongMacros.h"
Expand Down Expand Up @@ -132,20 +133,20 @@ - (void)setAutoFormattingBehavior:(STPFormTextFieldAutoFormattingBehavior)autoFo
return [inputString copy];
}
NSMutableAttributedString *attributedString = [inputString mutableCopy];
NSArray *cardSpacing;
STPCardBrand currentBrand = [STPCardValidator brandForNumber:attributedString.string];
if (currentBrand == STPCardBrandAmex) {
cardSpacing = @[@3, @9];
} else {
cardSpacing = @[@3, @7, @11];
}
for (NSUInteger i = 0; i < attributedString.length; i++) {
if ([cardSpacing containsObject:@(i)]) {
[attributedString addAttribute:NSKernAttributeName value:@(5)
range:NSMakeRange(i, 1)];
} else {
[attributedString addAttribute:NSKernAttributeName value:@(0)
range:NSMakeRange(i, 1)];
NSArray<NSNumber *> *cardNumberFormat = [STPCardValidator cardNumberFormatForBrand:currentBrand];

NSUInteger index = 0;
for (NSNumber *segmentLength in cardNumberFormat) {
NSUInteger segmentIndex = 0;
for (; index < attributedString.length && segmentIndex < [segmentLength unsignedIntegerValue]; index++, segmentIndex++) {
if (index + 1 != attributedString.length && segmentIndex + 1 == [segmentLength unsignedIntegerValue]) {
[attributedString addAttribute:NSKernAttributeName value:@(5)
range:NSMakeRange(index, 1)];
} else {
[attributedString addAttribute:NSKernAttributeName value:@(0)
range:NSMakeRange(index, 1)];
}
}
}
return [attributedString copy];
Expand Down
76 changes: 46 additions & 30 deletions Stripe/STPPaymentCardTextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#import "NSArray+Stripe.h"
#import "NSString+Stripe.h"
#import "STPCardValidator+Private.h"
#import "STPFormTextField.h"
#import "STPImageLibrary.h"
#import "STPPaymentCardTextFieldViewModel.h"
Expand Down Expand Up @@ -204,7 +205,7 @@ - (void)commonInit {
[brandImageView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:numberField
action:@selector(becomeFirstResponder)]];

self.focusedTextFieldForLayout = @(STPCardFieldTypeNumber);
self.focusedTextFieldForLayout = nil;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change because this causes the layout to force expiry and cvc to hidden if the screen isn't wide enough for full length number field and those fields. See line 1169, which still causes the number field to expand AND become the first responder (previously it was just expanded -- no first responder), however when the view first appears user will see all the fields, then an animation expanding the number field as it becomes first responder. Not a necessary change, but I thought it felt a lot better. Let me know your thoughts!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! (I haven't played with it yet)

[self updateCVCPlaceholder];
[self resetSubviewEditingTransitionState];
}
Expand Down Expand Up @@ -721,8 +722,19 @@ - (CGFloat)numberFieldFullWidth {
}

- (CGFloat)numberFieldCompressedWidth {
// Current longest possible longest pan fragment is 5 characters
return [self widthForText:@"88888"];

NSString *cardNumber = self.cardNumber;
if (cardNumber.length == 0) {
cardNumber = self.viewModel.defaultPlaceholder;
}

STPCardBrand currentBrand = [STPCardValidator brandForNumber:cardNumber];
NSArray<NSNumber *> *sortedCardNumberFormat = [[STPCardValidator cardNumberFormatForBrand:currentBrand] sortedArrayUsingSelector:@selector(unsignedIntegerValue)];
NSUInteger fragmentLength = [STPCardValidator fragmentLengthForCardBrand:currentBrand];
NSUInteger maxLength = MAX([[sortedCardNumberFormat lastObject] unsignedIntegerValue], fragmentLength);

NSString *maxCompressedString = [@"" stringByPaddingToLength:maxLength withString:@"8" startingAtIndex:0];
return [self widthForText:maxCompressedString];
}

- (CGFloat)cvcFieldWidth {
Expand Down Expand Up @@ -1052,39 +1064,37 @@ must be visible so they can be tapped over to (although
// cursor is at the end position the contents aren't clipped off to the left side
CGFloat additionalWidth = [self widthForText:@"8"];

width = [self numberFieldFullWidth]; // Number field is always actually full width, just sometimes clipped off to the left when "compressed"
if (panVisibility == STPCardTextFieldStateCompressed) {
// Need to lower xOffset so pan is partially off-screen

NSString *cardNumberToUse = self.cardNumber;
if (cardNumberToUse.length == 0) {
cardNumberToUse = self.viewModel.defaultPlaceholder;
}

NSUInteger length = [STPCardValidator fragmentLengthForCardBrand:[STPCardValidator brandForNumber:cardNumberToUse]];
NSUInteger toIndex = cardNumberToUse.length - length;

if (toIndex < cardNumberToUse.length) {
cardNumberToUse = [cardNumberToUse stp_safeSubstringToIndex:toIndex];
}
else {
cardNumberToUse = [self.viewModel.defaultPlaceholder stp_safeSubstringToIndex:toIndex];
BOOL hasEnteredCardNumber = self.cardNumber.length > 0;
NSString *compressedCardNumber = self.viewModel.compressedCardNumber;
NSString *cardNumberToHide = [(hasEnteredCardNumber ? self.cardNumber : self.viewModel.defaultPlaceholder) stp_stringByRemovingSuffix:compressedCardNumber];

if (cardNumberToHide.length > 0) {
width = hasEnteredCardNumber ? [self widthForCardNumber:self.cardNumber] : [self numberFieldFullWidth];

CGFloat hiddenWidth = [self widthForCardNumber:cardNumberToHide];
xOffset -= hiddenWidth;
UIView *maskView = [[UIView alloc] initWithFrame:CGRectMake(hiddenWidth,
0,
(width - hiddenWidth),
fieldsHeight)];
maskView.backgroundColor = [UIColor blackColor];
maskView.opaque = YES;
maskView.userInteractionEnabled = NO;
[UIView performWithoutAnimation:^{
self.numberField.maskView = maskView;
}];
} else {
width = [self numberFieldCompressedWidth];
[UIView performWithoutAnimation:^{
self.numberField.maskView = nil;
}];
}
CGFloat hiddenWidth = [self widthForCardNumber:cardNumberToUse];
xOffset -= hiddenWidth;
UIView *maskView = [[UIView alloc] initWithFrame:CGRectMake(hiddenWidth,
0,
(width - hiddenWidth),
fieldsHeight)];
maskView.backgroundColor = [UIColor blackColor];
maskView.opaque = YES;
maskView.userInteractionEnabled = NO;
[UIView performWithoutAnimation:^{
self.numberField.maskView = maskView;
}];

}
else {
width = [self numberFieldFullWidth];
[UIView performWithoutAnimation:^{
self.numberField.maskView = nil;
}];
Expand Down Expand Up @@ -1156,8 +1166,10 @@ - (void)layoutViewsToFocusField:(NSNumber *)focusedField
NSNumber *fieldtoFocus = focusedField;

if (fieldtoFocus == nil
&& ![self.focusedTextFieldForLayout isEqualToNumber:@(STPCardFieldTypeNumber)]
&& ([self.viewModel validationStateForField:STPCardFieldTypeNumber] != STPCardValidationStateValid)) {
fieldtoFocus = @(STPCardFieldTypeNumber);
[self.numberField becomeFirstResponder];
}

if ((fieldtoFocus == nil && self.focusedTextFieldForLayout == nil)
Expand Down Expand Up @@ -1389,6 +1401,7 @@ - (void)textFieldDidBeginEditing:(UITextField *)textField {

switch ((STPCardFieldType)textField.tag) {
case STPCardFieldTypeNumber:
((STPFormTextField *)textField).validText = YES;
if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingNumber:)]) {
[self.delegate paymentCardTextFieldDidBeginEditingNumber:self];
}
Expand Down Expand Up @@ -1423,6 +1436,9 @@ - (void)textFieldDidEndEditing:(UITextField *)textField {

switch ((STPCardFieldType)textField.tag) {
case STPCardFieldTypeNumber:
if ([self.viewModel validationStateForField:STPCardFieldTypeNumber] == STPCardValidationStateIncomplete) {
((STPFormTextField *)textField).validText = NO;
}
if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingNumber:)]) {
[self.delegate paymentCardTextFieldDidEndEditingNumber:self];
}
Expand Down
1 change: 1 addition & 0 deletions Stripe/STPPaymentCardTextFieldViewModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ typedef NS_ENUM(NSInteger, STPCardFieldType) {
@interface STPPaymentCardTextFieldViewModel : NSObject

@property (nonatomic, readwrite, copy, nullable) NSString *cardNumber;
@property (nonatomic, readonly, nullable) NSString *compressedCardNumber;
@property (nonatomic, readwrite, copy, nullable) NSString *rawExpiration;
@property (nonatomic, readonly, nullable) NSString *expirationMonth;
@property (nonatomic, readonly, nullable) NSString *expirationYear;
Expand Down
33 changes: 33 additions & 0 deletions Stripe/STPPaymentCardTextFieldViewModel.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "STPPaymentCardTextFieldViewModel.h"

#import "NSString+Stripe.h"
#import "STPCardValidator+Private.h"
#import "STPPostalCodeValidator.h"

@implementation STPPaymentCardTextFieldViewModel
Expand All @@ -20,6 +21,38 @@ - (void)setCardNumber:(NSString *)cardNumber {
_cardNumber = [sanitizedNumber stp_safeSubstringToIndex:maxLength];
}

- (NSString *)compressedCardNumber {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about adding some tests for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

NSString *cardNumber = self.cardNumber;
if (cardNumber.length == 0) {
cardNumber = self.defaultPlaceholder;
}

STPCardBrand currentBrand = [STPCardValidator brandForNumber:cardNumber];
if ([self validationStateForField:STPCardFieldTypeNumber] == STPCardValidationStateValid) {
// Use fragment length
NSUInteger length = [STPCardValidator fragmentLengthForCardBrand:currentBrand];
NSUInteger index = cardNumber.length - length;

if (index < cardNumber.length) {
return [cardNumber stp_safeSubstringFromIndex:index];
}
} else {
// use the card number format
NSArray<NSNumber *> *cardNumberFormat = [STPCardValidator cardNumberFormatForBrand:currentBrand];

NSUInteger index = 0;
for (NSNumber *segment in cardNumberFormat) {
NSUInteger segmentLength = [segment unsignedIntegerValue];
if (index + segmentLength >= cardNumber.length) {
return [cardNumber stp_safeSubstringFromIndex:index];
}
index += segmentLength;
}
}

return nil;
}

// This might contain slashes.
- (void)setRawExpiration:(NSString *)expiration {
NSString *sanitizedExpiration = [STPCardValidator sanitizedNumericStringForString:expiration];
Expand Down
Loading