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

STPPaymentCardTextField: changes return key behavior, adds willEndEditingForReturn delegate method #1059

Merged
merged 6 commits into from
Jan 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Stripe/PublicHeaders/STPPaymentCardTextField.h
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,20 @@ The curent brand image displayed in the receiver.
*/
- (void)paymentCardTextFieldDidBeginEditing:(nonnull STPPaymentCardTextField *)textField;

/**
Notification that the user pressed the `return` key after completely filling
out the STPPaymentCardTextField with data that passes validation.

The Stripe SDK is going to `resignFirstResponder` on the `STPPaymentCardTextField`
to dismiss the keyboard after this delegate method returns, however if your app wants
to do something more (ex: move first responder to another field), this is a good
opportunity to do that.

This is delivered *before* the corresponding `paymentCardTextFieldDidEndEditing:`

@param textField The STPPaymentCardTextField that was being edited when the user pressed return
*/
- (void)paymentCardTextFieldWillEndEditingForReturn:(nonnull STPPaymentCardTextField *)textField;

/**
Called when editing ends in the text field as a whole.
Expand Down
4 changes: 4 additions & 0 deletions Stripe/STPAddCardViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ - (void)paymentFieldNextTapped {
[[self.addressViewModel.addressCells stp_boundSafeObjectAtIndex:0] becomeFirstResponder];
}

- (void)paymentCardTextFieldWillEndEditingForReturn:(__unused STPPaymentCardTextField *)textField {
[self paymentFieldNextTapped];
}

- (void)paymentCardTextFieldDidBeginEditingCVC:(STPPaymentCardTextField *)textField {
BOOL isAmex = [STPCardValidator brandForNumber:textField.cardNumber] == STPCardBrandAmex;
UIImage *newImage;
Expand Down
32 changes: 31 additions & 1 deletion Stripe/STPPaymentCardTextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,20 @@ - (BOOL)becomeFirstResponder {
return [firstResponder becomeFirstResponder];
}

/**
Returns the next text field to be edited, in priority order:

1. If we're currently in a text field, returns the next one (ignoring postalCodeField if postalCodeEntryEnabled == NO)
2. Otherwise, returns the first invalid field (either cycling back from the end or as it gains 1st responder)
3. As a final fallback, just returns the last field
*/
- (nonnull STPFormTextField *)nextFirstResponderField {
STPFormTextField *currentFirstResponder = [self currentFirstResponderField];
if (currentFirstResponder) {
NSUInteger index = [self.allFields indexOfObject:currentFirstResponder];
if (index != NSNotFound) {
STPFormTextField *nextField = [self.allFields stp_boundSafeObjectAtIndex:index + 1];
if (self.postalCodeEntryEnabled || nextField != self.postalCodeField) {
if (nextField != nil && (self.postalCodeEntryEnabled || nextField != self.postalCodeField)) {
return nextField;
}
}
Expand Down Expand Up @@ -1322,6 +1329,14 @@ - (void)formTextFieldTextDidChange:(STPFormTextField *)formTextField {
if (sanitizedCvc.length < [STPCardValidator maxCVCLengthForCardBrand:self.viewModel.brand]) {
break;
}
} else if (fieldType == STPCardFieldTypePostalCode) {
/*
Similar to the UX problems on CVC, since our Postal Code validation
is pretty light, we want to block auto-advance here. In the US, this
allows users to enter 9 digit zips if they want, and as many as they
need in non-US countries (where >0 characters is "valid")
*/
break;
}

// This is a no-op if this is the last field & they're all valid
Expand Down Expand Up @@ -1478,6 +1493,21 @@ - (void)textFieldDidEndEditing:(UITextField *)textField {
}
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (textField == [self lastSubField] && [self firstInvalidSubField] == nil) {
// User pressed return in the last field, and all fields are valid
if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldWillEndEditingForReturn:)]) {
[self.delegate paymentCardTextFieldWillEndEditingForReturn:self];
}
[self resignFirstResponder];
} else {
// otherwise, move to the next field
[[self nextFirstResponderField] becomeFirstResponder];
}

return NO;
}

- (UIImage *)brandImage {
STPCardFieldType fieldType = STPCardFieldTypeNumber;
if (self.currentFirstResponderField) {
Expand Down
66 changes: 65 additions & 1 deletion Tests/Tests/STPPaymentCardTextFieldTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,25 @@ + (UIImage *)brandImageForCardBrand:(STPCardBrand)cardBrand;
*/
@interface PaymentCardTextFieldBlockDelegate: NSObject <STPPaymentCardTextFieldDelegate>
@property (nonatomic, strong, nullable) void (^didChange)(STPPaymentCardTextField *);
@property (nonatomic, strong, nullable) void (^willEndEditingForReturn)(STPPaymentCardTextField *);
@property (nonatomic, strong, nullable) void (^didEndEditing)(STPPaymentCardTextField *);
// add more properties for other delegate methods as this test needs them
@end
@implementation PaymentCardTextFieldBlockDelegate
- (void)paymentCardTextFieldDidChange:(STPPaymentCardTextField *)textField {
self.didChange(textField);
if (self.didChange) {
self.didChange(textField);
}
}
- (void)paymentCardTextFieldWillEndEditingForReturn:(STPPaymentCardTextField *)textField {
if (self.willEndEditingForReturn) {
self.willEndEditingForReturn(textField);
}
}
- (void)paymentCardTextFieldDidEndEditing:(STPPaymentCardTextField *)textField {
if (self.didEndEditing) {
self.didEndEditing(textField);
}
}
@end

Expand Down Expand Up @@ -600,4 +614,54 @@ - (void)testBecomeFirstResponder {
@"When all fields are valid, the last one should be the preferred firstResponder");
}

- (void)testShouldReturnCyclesThroughFields {
PaymentCardTextFieldBlockDelegate *delegate = [PaymentCardTextFieldBlockDelegate new];
delegate.willEndEditingForReturn = ^(__unused STPPaymentCardTextField *textField) {
XCTFail(@"Did not expect editing to end in this test");
};
self.sut.delegate = delegate;

[self.sut becomeFirstResponder];
XCTAssertTrue(self.sut.numberField.isFirstResponder);

XCTAssertFalse([self.sut.numberField.delegate textFieldShouldReturn:self.sut.numberField], @"shouldReturn = NO");
XCTAssertTrue(self.sut.expirationField.isFirstResponder, @"with side effect to move 1st responder to next field");

XCTAssertFalse([self.sut.expirationField.delegate textFieldShouldReturn:self.sut.expirationField], @"shouldReturn = NO");
XCTAssertTrue(self.sut.cvcField.isFirstResponder, @"with side effect to move 1st responder to next field");

XCTAssertFalse([self.sut.cvcField.delegate textFieldShouldReturn:self.sut.cvcField], @"shouldReturn = NO");
XCTAssertTrue(self.sut.numberField.isFirstResponder, @"with side effect to move 1st responder from last field to first invalid field");
}

- (void)testShouldReturnDismissesWhenValid {
__block BOOL hasReturned = NO;
__block BOOL didEnd = NO;

[self.sut setCardParams:[STPFixtures cardParams]];

PaymentCardTextFieldBlockDelegate *delegate = [PaymentCardTextFieldBlockDelegate new];
delegate.willEndEditingForReturn = ^(__unused STPPaymentCardTextField *textField) {
XCTAssertFalse(didEnd, @"willEnd is called before didEnd");
XCTAssertFalse(hasReturned, @"willEnd is only called once");
hasReturned = YES;
};
delegate.didEndEditing = ^(__unused STPPaymentCardTextField *textField) {
XCTAssertTrue(hasReturned, @"didEndEditing should be called after willEnd");
XCTAssertFalse(didEnd, @"didEnd is only called once");
didEnd = YES;
};

self.sut.delegate = delegate;
[self.sut becomeFirstResponder];
XCTAssertTrue(self.sut.cvcField.isFirstResponder, @"when textfield is filled out, default first responder is the last field");

XCTAssertFalse(hasReturned, @"willEndEditingForReturn delegate method should not have been called yet");
XCTAssertFalse([self.sut.cvcField.delegate textFieldShouldReturn:self.sut.cvcField], @"shouldReturn = NO");

XCTAssertNil(self.sut.currentFirstResponderField, @"Should have resigned first responder");
XCTAssertTrue(hasReturned, @"delegate method has been invoked");
XCTAssertTrue(didEnd, @"delegate method has been invoked");
}

@end