-
Notifications
You must be signed in to change notification settings - Fork 983
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
Changes from all commits
ced32c4
46e77af
27ffb51
4e80ca0
ee61f36
b262127
964dcb2
2924168
25e7846
adaae56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); |
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]; | ||
default: | ||
return @[@4, @4, @4, @4]; | ||
} | ||
} | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END | ||
|
||
void linkSTPCardValidatorPrivateCategory(void){} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -204,7 +205,7 @@ - (void)commonInit { | |
[brandImageView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:numberField | ||
action:@selector(becomeFirstResponder)]]; | ||
|
||
self.focusedTextFieldForLayout = @(STPCardFieldTypeNumber); | ||
self.focusedTextFieldForLayout = nil; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||
} | ||
|
@@ -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 { | ||
|
@@ -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; | ||
}]; | ||
|
@@ -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) | ||
|
@@ -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]; | ||
} | ||
|
@@ -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]; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
#import "STPPaymentCardTextFieldViewModel.h" | ||
|
||
#import "NSString+Stripe.h" | ||
#import "STPCardValidator+Private.h" | ||
#import "STPPostalCodeValidator.h" | ||
|
||
@implementation STPPaymentCardTextFieldViewModel | ||
|
@@ -20,6 +21,38 @@ - (void)setCardNumber:(NSString *)cardNumber { | |
_cardNumber = [sanitizedNumber stp_safeSubstringToIndex:maxLength]; | ||
} | ||
|
||
- (NSString *)compressedCardNumber { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT about adding some tests for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fragmentLengthForCardBrand:
returns2
for Diners club. Should it do something different? Or should this end in2
?Should
fragmentLengthForCardBrand:
call this method and just return the last element of the array?There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 offragmentLengthForCardBrand: