19 Card Digit Support Part 2 (#1631)
* * Adds API bindings for new, private card metadata endpoint * Disables card number truncation in `STPPaymentCardTextField` * Disables auto-advancing out of the number entry field in `STPPaymentCardTextField` * Updates card number validation to wait for response from card metadata endpoint * Updates tests with new card number behavior and asynchronicity * Fixes UI test * Enables auto-advancing when able and loading indicator to STPPaymentCardTextField * Revert * Comment updates from review * Adds logging for entering full card number before getting a network response. * Adds additional logging. * Return * Use configuration * Fixes inifite loop; fixes KVO; fixes tests for updated behavior. * Fixes memory leak/crash from retain cycle in OCMock code * Fix test case * Only enable service-based BIN lookups for CUP bins (#1642) * Fixes validation tests and removes delay from card textfield tests. * Updates README with correct simulator version for tests. * Fixes snapshot tests Co-authored-by: davidme-stripe <52758633+davidme-stripe@users.noreply.github.com>
This commit is contained in:
parent
e8f53b2943
commit
03ab0f2280
|
@ -111,7 +111,7 @@ We welcome contributions of any kind including new features, bug fixes, and docu
|
|||
1. Install Carthage (if you have homebrew installed, `brew install carthage`)
|
||||
2. From the root of the repo, install test dependencies by running `carthage bootstrap --platform ios --configuration Release --no-use-binaries`
|
||||
3. Open Stripe.xcworkspace
|
||||
4. Choose the "StripeiOS" scheme with the iPhone 7, iOS 12.2 simulator (required for snapshot tests to pass)
|
||||
4. Choose the "StripeiOS" scheme with the iPhone 8, iOS 13.6 simulator (required for snapshot tests to pass)
|
||||
5. Run Product -> Test
|
||||
|
||||
## Migrating from Older Versions
|
||||
|
|
|
@ -276,6 +276,8 @@
|
|||
363E25DA24198B0D00070D59 /* STPViewWithSeparator.m in Sources */ = {isa = PBXBuildFile; fileRef = 363E25D824198B0D00070D59 /* STPViewWithSeparator.m */; };
|
||||
363EA802241C3EC8000C7671 /* STPAUBECSDebitFormViewSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 363EA801241C3EC8000C7671 /* STPAUBECSDebitFormViewSnapshotTests.m */; };
|
||||
363EA804241C4346000C7671 /* STPAUBECSDebitFormView+Testing.h in Headers */ = {isa = PBXBuildFile; fileRef = 363EA803241C4346000C7671 /* STPAUBECSDebitFormView+Testing.h */; };
|
||||
364B75DD24F46354007D9FAB /* STPCardLoadingIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 364B75DB24F46354007D9FAB /* STPCardLoadingIndicator.h */; };
|
||||
364B75DE24F46354007D9FAB /* STPCardLoadingIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = 364B75DC24F46354007D9FAB /* STPCardLoadingIndicator.m */; };
|
||||
3650AA4221C07E3C002B0893 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3650AA4121C07E3C002B0893 /* AppDelegate.m */; };
|
||||
3650AA4521C07E3C002B0893 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3650AA4421C07E3C002B0893 /* ViewController.m */; };
|
||||
3650AA4A21C07E3D002B0893 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3650AA4921C07E3D002B0893 /* Assets.xcassets */; };
|
||||
|
@ -1067,6 +1069,8 @@
|
|||
363E25D824198B0D00070D59 /* STPViewWithSeparator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPViewWithSeparator.m; sourceTree = "<group>"; };
|
||||
363EA801241C3EC8000C7671 /* STPAUBECSDebitFormViewSnapshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPAUBECSDebitFormViewSnapshotTests.m; sourceTree = "<group>"; };
|
||||
363EA803241C4346000C7671 /* STPAUBECSDebitFormView+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPAUBECSDebitFormView+Testing.h"; sourceTree = "<group>"; };
|
||||
364B75DB24F46354007D9FAB /* STPCardLoadingIndicator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STPCardLoadingIndicator.h; sourceTree = "<group>"; };
|
||||
364B75DC24F46354007D9FAB /* STPCardLoadingIndicator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPCardLoadingIndicator.m; sourceTree = "<group>"; };
|
||||
3650AA3E21C07E3C002B0893 /* LocalizationTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LocalizationTester.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3650AA4021C07E3C002B0893 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
3650AA4121C07E3C002B0893 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
|
@ -2807,8 +2811,17 @@
|
|||
F1728CF81EAAA945002E0C29 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
363EA803241C4346000C7671 /* STPAUBECSDebitFormView+Testing.h */,
|
||||
36B8DDE8241AEB5400BB908E /* STPAUBECSFormViewModel.h */,
|
||||
36B8DDE9241AEB5400BB908E /* STPAUBECSFormViewModel.m */,
|
||||
364B75DB24F46354007D9FAB /* STPCardLoadingIndicator.h */,
|
||||
364B75DC24F46354007D9FAB /* STPCardLoadingIndicator.m */,
|
||||
0438EF261B7416BB00D506CC /* STPFormTextField.h */,
|
||||
0438EF271B7416BB00D506CC /* STPFormTextField.m */,
|
||||
36B8DDE0241AC0A100BB908E /* STPLabeledFormTextFieldView.h */,
|
||||
36B8DDE1241AC0A100BB908E /* STPLabeledFormTextFieldView.m */,
|
||||
36B8DDE4241AC17200BB908E /* STPLabeledMultiFormTextFieldView.h */,
|
||||
36B8DDE5241AC17200BB908E /* STPLabeledMultiFormTextFieldView.m */,
|
||||
0438EF2A1B7416BB00D506CC /* STPPaymentCardTextFieldViewModel.h */,
|
||||
0438EF2B1B7416BB00D506CC /* STPPaymentCardTextFieldViewModel.m */,
|
||||
C158AB3D1E1EE98900348D01 /* STPSectionHeaderView.h */,
|
||||
|
@ -2817,13 +2830,6 @@
|
|||
B347DD471FE35423006B3BAC /* STPValidatedTextField.m */,
|
||||
363E25D724198B0D00070D59 /* STPViewWithSeparator.h */,
|
||||
363E25D824198B0D00070D59 /* STPViewWithSeparator.m */,
|
||||
36B8DDE0241AC0A100BB908E /* STPLabeledFormTextFieldView.h */,
|
||||
36B8DDE1241AC0A100BB908E /* STPLabeledFormTextFieldView.m */,
|
||||
36B8DDE4241AC17200BB908E /* STPLabeledMultiFormTextFieldView.h */,
|
||||
36B8DDE5241AC17200BB908E /* STPLabeledMultiFormTextFieldView.m */,
|
||||
36B8DDE8241AEB5400BB908E /* STPAUBECSFormViewModel.h */,
|
||||
36B8DDE9241AEB5400BB908E /* STPAUBECSFormViewModel.m */,
|
||||
363EA803241C4346000C7671 /* STPAUBECSDebitFormView+Testing.h */,
|
||||
);
|
||||
name = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2928,6 +2934,7 @@
|
|||
B61D4B97245767D2001AEBEF /* STPPaymentIntentShippingDetailsAddress.h in Headers */,
|
||||
C158AB3F1E1EE98900348D01 /* STPSectionHeaderView.h in Headers */,
|
||||
04EBC7571B7533C300A0E6AE /* STPCardValidator.h in Headers */,
|
||||
364B75DD24F46354007D9FAB /* STPCardLoadingIndicator.h in Headers */,
|
||||
366658B0240F20AD00D00354 /* STPPaymentMethodAUBECSDebit.h in Headers */,
|
||||
36B8DDE2241AC0A100BB908E /* STPLabeledFormTextFieldView.h in Headers */,
|
||||
046FE9A11CE55D1D00DA6A7B /* STPPaymentActivityIndicatorView.h in Headers */,
|
||||
|
@ -3776,6 +3783,7 @@
|
|||
311A475824EB1D2300576D92 /* STPCardScanner.m in Sources */,
|
||||
F1BEB2FF1F3508BB0043F48C /* NSError+Stripe.m in Sources */,
|
||||
B699BC9524577FED009107F9 /* STPPaymentIntentShippingDetailsAddressParams.m in Sources */,
|
||||
364B75DE24F46354007D9FAB /* STPCardLoadingIndicator.m in Sources */,
|
||||
049952D01BCF13510088C703 /* STPAPIRequest.m in Sources */,
|
||||
F1A2F92E1EEB6A70006B0456 /* NSCharacterSet+Stripe.m in Sources */,
|
||||
F1D3A24B1EB012010095BFA9 /* STPFile.m in Sources */,
|
||||
|
|
|
@ -72,6 +72,16 @@
|
|||
intentID:(NSString *)intentID
|
||||
errorDictionary:(NSDictionary *)errorDictionary;
|
||||
|
||||
#pragma mark - Card Metadata
|
||||
|
||||
- (void)logUserEnteredCompletePANBeforeMetadataLoadedWithConfiguration:(STPPaymentConfiguration *)configuration;
|
||||
|
||||
- (void)logCardMetadataResponseFailureWithConfiguration:(STPPaymentConfiguration *)configuration;
|
||||
|
||||
- (void)logCardMetadataMissingRangeWithConfiguration:(STPPaymentConfiguration *)configuration;
|
||||
|
||||
#pragma mark - Card Scanning
|
||||
|
||||
- (void)logCardScanSucceededWithDuration:(NSTimeInterval)duration;
|
||||
|
||||
- (void)logCardScanCancelledWithDuration:(NSTimeInterval)duration;
|
||||
|
|
|
@ -321,6 +321,43 @@
|
|||
[self logPayload:payload];
|
||||
}
|
||||
|
||||
#pragma mark - Card Metadata
|
||||
|
||||
- (void)logUserEnteredCompletePANBeforeMetadataLoadedWithConfiguration:(STPPaymentConfiguration *)configuration {
|
||||
NSDictionary *configurationDictionary = [self.class serializeConfiguration:configuration];
|
||||
NSMutableDictionary *payload = [self.class commonPayload];
|
||||
[payload addEntriesFromDictionary:@{
|
||||
@"event": @"stripeios.card_metadata_loaded_too_slow",
|
||||
}];
|
||||
[payload addEntriesFromDictionary:[self productUsageDictionary]];
|
||||
[payload addEntriesFromDictionary:configurationDictionary];
|
||||
[self logPayload:payload];
|
||||
}
|
||||
|
||||
- (void)logCardMetadataResponseFailureWithConfiguration:(STPPaymentConfiguration *)configuration {
|
||||
NSDictionary *configurationDictionary = [self.class serializeConfiguration:configuration];
|
||||
NSMutableDictionary *payload = [self.class commonPayload];
|
||||
[payload addEntriesFromDictionary:@{
|
||||
@"event": @"stripeios.card_metadata_load_failure",
|
||||
}];
|
||||
[payload addEntriesFromDictionary:[self productUsageDictionary]];
|
||||
[payload addEntriesFromDictionary:configurationDictionary];
|
||||
[self logPayload:payload];
|
||||
}
|
||||
|
||||
- (void)logCardMetadataMissingRangeWithConfiguration:(STPPaymentConfiguration *)configuration {
|
||||
NSDictionary *configurationDictionary = [self.class serializeConfiguration:configuration];
|
||||
NSMutableDictionary *payload = [self.class commonPayload];
|
||||
[payload addEntriesFromDictionary:@{
|
||||
@"event": @"stripeios.card_metadata_missing_range",
|
||||
}];
|
||||
[payload addEntriesFromDictionary:[self productUsageDictionary]];
|
||||
[payload addEntriesFromDictionary:configurationDictionary];
|
||||
[self logPayload:payload];
|
||||
}
|
||||
|
||||
#pragma mark - Card Scanning
|
||||
|
||||
- (void)logCardScanSucceededWithDuration:(NSTimeInterval)duration {
|
||||
NSMutableDictionary *payload = [self.class commonPayload];
|
||||
[payload addEntriesFromDictionary:@{
|
||||
|
|
|
@ -23,12 +23,22 @@ typedef void (^STPRetrieveBINRangesCompletionBlock)(NSArray<STPBINRange *> * _Nu
|
|||
@property (nonatomic, readonly, copy) NSString *qRangeLow;
|
||||
@property (nonatomic, readonly, copy) NSString *qRangeHigh;
|
||||
@property (nonatomic, nullable, readonly) NSString *country;
|
||||
@property (nonatomic, readonly) BOOL isCardMetadata; // indicates bin range was downloaded from edge service
|
||||
|
||||
+ (BOOL)isLoadingCardMetadataForPrefix:(NSString *)binPrefix;
|
||||
|
||||
|
||||
+ (NSArray<STPBINRange *> *)allRanges;
|
||||
+ (NSArray<STPBINRange *> *)binRangesForNumber:(NSString *)number;
|
||||
+ (NSArray<STPBINRange *> *)binRangesForBrand:(STPCardBrand)brand;
|
||||
+ (instancetype)mostSpecificBINRangeForNumber:(NSString *)number;
|
||||
|
||||
+ (NSUInteger)maxCardNumberLength;
|
||||
+ (NSUInteger)minLengthForFullBINRange;
|
||||
|
||||
+ (BOOL)hasBINRangesForPrefix:(NSString *)binPrefix;
|
||||
+ (BOOL)isInvalidBINPrefix:(NSString *)binPrefix;
|
||||
|
||||
// This will asynchronously check if we have already fetched metadata for this prefix and if we have not will
|
||||
// issue a network request to retrieve it if possible.
|
||||
+ (void)retrieveBINRangesForPrefix:(NSString *)binPrefix completion:(STPRetrieveBINRangesCompletionBlock)completion;
|
||||
|
|
|
@ -10,12 +10,17 @@
|
|||
|
||||
#import "NSDictionary+Stripe.h"
|
||||
#import "NSString+Stripe.h"
|
||||
#import "STPAnalyticsClient.h"
|
||||
#import "STPAPIClient+Private.h"
|
||||
#import "STPCard+Private.h"
|
||||
#import "STPCardBINMetadata.h"
|
||||
#import "STPPaymentConfiguration.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static const NSUInteger kMaxCardNumberLength = 19;
|
||||
static const NSUInteger kPrefixLengthForMetadataRequest = 6;
|
||||
|
||||
@interface STPBINRange()
|
||||
|
||||
@property (nonatomic) NSUInteger length;
|
||||
|
@ -90,12 +95,30 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
binRange.brand = [STPCard brandFromString:brandString];
|
||||
binRange.length = [length unsignedIntegerValue];
|
||||
binRange.country = [dict stp_stringForKey:@"country"];
|
||||
binRange->_isCardMetadata = YES;
|
||||
|
||||
return binRange;
|
||||
}
|
||||
|
||||
#pragma mark - Class Utilities
|
||||
|
||||
+ (BOOL)isLoadingCardMetadataForPrefix:(NSString *)binPrefix {
|
||||
__block BOOL isLoading = NO;
|
||||
dispatch_sync([self _retrievalQueue], ^{
|
||||
NSString *binPrefixKey = [binPrefix stp_safeSubstringToIndex:kPrefixLengthForMetadataRequest];
|
||||
isLoading = binPrefixKey != nil && sPendingRequests[binPrefixKey] != nil;
|
||||
});
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxCardNumberLength {
|
||||
return kMaxCardNumberLength;
|
||||
}
|
||||
|
||||
+ (NSUInteger)minLengthForFullBINRange {
|
||||
return kPrefixLengthForMetadataRequest;
|
||||
}
|
||||
|
||||
static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
||||
|
||||
+ (void)_performSyncWithAllRangesLock:(dispatch_block_t)block {
|
||||
|
@ -110,7 +133,7 @@ static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
|||
if (STPBINRangeAllRanges == nil) {
|
||||
NSArray *ranges = @[
|
||||
// Unknown
|
||||
@[@"", @"", @16, @(STPCardBrandUnknown)],
|
||||
@[@"", @"", @19, @(STPCardBrandUnknown)],
|
||||
|
||||
// American Express
|
||||
@[@"34", @"34", @15, @(STPCardBrandAmex)],
|
||||
|
@ -135,6 +158,7 @@ static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
|||
|
||||
// UnionPay
|
||||
@[@"62", @"62", @16, @(STPCardBrandUnionPay)],
|
||||
@[@"81", @"81", @16, @(STPCardBrandUnionPay)],
|
||||
|
||||
// Visa
|
||||
@[@"40", @"49", @16, @(STPCardBrandVisa)],
|
||||
|
@ -203,12 +227,13 @@ static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
|||
}]];
|
||||
}
|
||||
|
||||
+ (void)retrieveBINRangesForPrefix:(NSString *)binPrefix completion:(STPRetrieveBINRangesCompletionBlock)completion {
|
||||
// sPendingRequests contains the completion blocks for a given metadata request that we have not yet gotten a response for
|
||||
static NSMutableDictionary<NSString *, NSArray<STPRetrieveBINRangesCompletionBlock> *> *sPendingRequests = nil;
|
||||
// sRetrievedRanges tracks the bin prefixes for which we've already received metadata responses
|
||||
static NSMutableDictionary<NSString *, NSArray<STPBINRange *> *> *sRetrievedRanges = nil;
|
||||
// sRetrievalQueue protects access to the two above dictionaries, sSpendingRequests and sRetrievedRanges
|
||||
|
||||
// _retrievalQueue protects access to the two above dictionaries, sSpendingRequests and sRetrievedRanges
|
||||
+ (dispatch_queue_t)_retrievalQueue {
|
||||
static dispatch_queue_t sRetrievalQueue = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
|
@ -218,10 +243,42 @@ static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
|||
sRetrievedRanges = [NSMutableDictionary new];
|
||||
});
|
||||
|
||||
dispatch_async(sRetrievalQueue, ^{
|
||||
NSString *binPrefixKey = [binPrefix stp_safeSubstringToIndex:6];
|
||||
if (sRetrievedRanges[binPrefixKey] != nil || binPrefixKey.length < 6) {
|
||||
return sRetrievalQueue;
|
||||
}
|
||||
|
||||
+ (BOOL)hasBINRangesForPrefix:(NSString *)binPrefix {
|
||||
if ([self isInvalidBINPrefix:binPrefix]) {
|
||||
return YES; // we won't fetch any more info for this prefix
|
||||
}
|
||||
if (![self isVariableLengthBINPrefix:binPrefix]) {
|
||||
return YES; // if we know a card has a static length, we don't need to ask the BIN service
|
||||
}
|
||||
__block BOOL hasBINRanges = NO;
|
||||
dispatch_sync([self _retrievalQueue], ^{
|
||||
NSString *binPrefixKey = [binPrefix stp_safeSubstringToIndex:kPrefixLengthForMetadataRequest];
|
||||
hasBINRanges = binPrefixKey.length == kPrefixLengthForMetadataRequest && sRetrievedRanges[binPrefixKey] != nil;
|
||||
});
|
||||
return hasBINRanges;
|
||||
}
|
||||
|
||||
+ (BOOL)isInvalidBINPrefix:(NSString *)binPrefix {
|
||||
NSString *firstFive = [binPrefix stp_safeSubstringToIndex:kPrefixLengthForMetadataRequest - 1];
|
||||
return ((STPBINRange *)[self mostSpecificBINRangeForNumber:firstFive]).brand == STPCardBrandUnknown;
|
||||
}
|
||||
|
||||
+ (BOOL)isVariableLengthBINPrefix:(NSString *)binPrefix {
|
||||
NSString *firstFive = [binPrefix stp_safeSubstringToIndex:kPrefixLengthForMetadataRequest - 1];
|
||||
// Only UnionPay has variable-length cards at the moment.
|
||||
return ((STPBINRange *)[self mostSpecificBINRangeForNumber:firstFive]).brand == STPCardBrandUnionPay;
|
||||
}
|
||||
|
||||
+ (void)retrieveBINRangesForPrefix:(NSString *)binPrefix completion:(STPRetrieveBINRangesCompletionBlock)completion {
|
||||
|
||||
dispatch_async([self _retrievalQueue], ^{
|
||||
NSString *binPrefixKey = [binPrefix stp_safeSubstringToIndex:kPrefixLengthForMetadataRequest];
|
||||
if (sRetrievedRanges[binPrefixKey] != nil || binPrefixKey.length < kPrefixLengthForMetadataRequest || [self isInvalidBINPrefix:binPrefixKey]) {
|
||||
// if we already have a metadata response or the binPrefix isn't long enough to make a request,
|
||||
// or we know that this is not a valid BIN prefix
|
||||
// return the bin ranges we already have on device
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
completion([self binRangesForNumber:binPrefix], nil);
|
||||
|
@ -234,7 +291,7 @@ static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
|||
sPendingRequests[binPrefixKey] = @[[completion copy]];
|
||||
[[STPAPIClient sharedClient] retrieveCardBINMetadataForPrefix:binPrefixKey
|
||||
withCompletion:^(STPCardBINMetadata * _Nullable cardMetadata, NSError * _Nullable error) {
|
||||
dispatch_async(sRetrievalQueue, ^{
|
||||
dispatch_async([self _retrievalQueue], ^{
|
||||
NSArray<STPBINRange *> *ranges = cardMetadata.ranges;
|
||||
NSArray<STPRetrieveBINRangesCompletionBlock> *completionBlocks = sPendingRequests[binPrefixKey];
|
||||
|
||||
|
@ -244,6 +301,8 @@ static NSArray<STPBINRange *> *STPBINRangeAllRanges = nil;
|
|||
[self _performSyncWithAllRangesLock:^{
|
||||
STPBINRangeAllRanges = [STPBINRangeAllRanges arrayByAddingObjectsFromArray:ranges];
|
||||
}];
|
||||
} else {
|
||||
[[STPAnalyticsClient sharedClient] logCardMetadataResponseFailureWithConfiguration:[STPPaymentConfiguration sharedConfiguration]];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
|
|
@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSString *brand = [string lowercaseString];
|
||||
if ([brand isEqualToString:@"visa"]) {
|
||||
return STPCardBrandVisa;
|
||||
} else if ([brand isEqualToString:@"american express"]) {
|
||||
} else if ([brand isEqualToString:@"american express"] || [brand isEqualToString:@"american_express"]) {
|
||||
return STPCardBrandAmex;
|
||||
} else if ([brand isEqualToString:@"mastercard"]) {
|
||||
return STPCardBrandMasterCard;
|
||||
|
@ -53,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return STPCardBrandDiscover;
|
||||
} else if ([brand isEqualToString:@"jcb"]) {
|
||||
return STPCardBrandJCB;
|
||||
} else if ([brand isEqualToString:@"diners club"]) {
|
||||
} else if ([brand isEqualToString:@"diners club"] || [brand isEqualToString:@"diners_club"]) {
|
||||
return STPCardBrandDinersClub;
|
||||
} else if ([brand isEqualToString:@"unionpay"]) {
|
||||
return STPCardBrandUnionPay;
|
||||
|
|
|
@ -34,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// STPCardLoadingIndicator.h
|
||||
// StripeiOS
|
||||
//
|
||||
// Created by Cameron Sabol on 8/24/20.
|
||||
// Copyright © 2020 Stripe, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface STPCardLoadingIndicator : UIView
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// STPCardLoadingIndicator.m
|
||||
// StripeiOS
|
||||
//
|
||||
// Created by Cameron Sabol on 8/24/20.
|
||||
// Copyright © 2020 Stripe, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#import "STPCardLoadingIndicator.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static const CGFloat kCardLoadingIndicatorDiameter = 14.f;
|
||||
static const CGFloat kCardLoadingInnerCircleDiameter = 10.f;
|
||||
static const CFTimeInterval kLoadingAnimationSpinDuration = 0.6;
|
||||
|
||||
static NSString * const kLoadingAnimationIdentifier = @"STPCardLoadingIndicator.spinning";
|
||||
|
||||
@implementation STPCardLoadingIndicator {
|
||||
CALayer *_indicatorLayer;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor colorWithRed:79.f/255.f green:86.f/255.f blue:107.f/255.f alpha:1.f];
|
||||
|
||||
// Make us a circle
|
||||
CAShapeLayer *shape = [CAShapeLayer layer];
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0.5f*kCardLoadingIndicatorDiameter, 0.5f*kCardLoadingIndicatorDiameter)
|
||||
radius:0.5f*kCardLoadingIndicatorDiameter
|
||||
startAngle:0.f
|
||||
endAngle:(2.f*M_PI)
|
||||
clockwise:YES];
|
||||
shape.path = path.CGPath;
|
||||
self.layer.mask = shape;
|
||||
|
||||
// Add the inner circle
|
||||
CAShapeLayer *innerCircle = [CAShapeLayer layer];
|
||||
innerCircle.anchorPoint = CGPointMake(0.5f, 0.5f);
|
||||
innerCircle.position = CGPointMake(0.5f*kCardLoadingIndicatorDiameter, 0.5f*kCardLoadingIndicatorDiameter);
|
||||
|
||||
UIBezierPath *indicatorPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0.f, 0.f)
|
||||
radius:0.5f*kCardLoadingInnerCircleDiameter
|
||||
startAngle:0.f
|
||||
endAngle:(9.f*M_PI/6.f)
|
||||
clockwise:YES];
|
||||
innerCircle.path = indicatorPath.CGPath;
|
||||
innerCircle.strokeColor = [UIColor colorWithWhite:1.f alpha:0.8f].CGColor;
|
||||
innerCircle.fillColor = [UIColor clearColor].CGColor;
|
||||
[self.layer addSublayer:innerCircle];
|
||||
_indicatorLayer = (CALayer *)innerCircle;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(kCardLoadingIndicatorDiameter, kCardLoadingIndicatorDiameter);
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self startAnimating];
|
||||
}
|
||||
|
||||
- (void)startAnimating {
|
||||
CABasicAnimation *spinAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
|
||||
spinAnimation.byValue = [NSNumber numberWithFloat:(float)(2.0f*M_PI)];
|
||||
spinAnimation.duration = kLoadingAnimationSpinDuration;
|
||||
spinAnimation.repeatCount = INFINITY;
|
||||
|
||||
[_indicatorLayer addAnimation:spinAnimation forKey:kLoadingAnimationIdentifier];
|
||||
}
|
||||
|
||||
- (void)stopAnimating {
|
||||
[_indicatorLayer removeAnimationForKey:kLoadingAnimationIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
+ (NSArray<NSNumber *> *)cardNumberFormatForBrand:(STPCardBrand)brand;
|
||||
+ (NSArray<NSNumber *> *)cardNumberFormatForCardNumber:(NSString *)cardNumber;
|
||||
|
||||
+ (BOOL)stringIsValidLuhn:(NSString *)number;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -33,6 +33,25 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [self cardNumberFormatForBrand:binRange.brand];
|
||||
}
|
||||
|
||||
+ (BOOL)stringIsValidLuhn:(NSString *)number {
|
||||
BOOL odd = true;
|
||||
int sum = 0;
|
||||
NSMutableArray *digits = [NSMutableArray arrayWithCapacity:number.length];
|
||||
|
||||
for (int i = 0; i < (NSInteger)number.length; i++) {
|
||||
[digits addObject:[number substringWithRange:NSMakeRange(i, 1)]];
|
||||
}
|
||||
|
||||
for (NSString *digitStr in [digits reverseObjectEnumerator]) {
|
||||
int digit = [digitStr intValue];
|
||||
if ((odd = !odd)) digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
sum += digit;
|
||||
}
|
||||
|
||||
return sum % 10 == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
#import "STPCardValidator.h"
|
||||
#import "STPCardValidator+Private.h"
|
||||
|
||||
#import "STPAnalyticsClient.h"
|
||||
#import "STPBINRange.h"
|
||||
#import "STPPaymentConfiguration.h"
|
||||
#import "NSCharacterSet+Stripe.h"
|
||||
|
||||
@implementation STPCardValidator
|
||||
|
@ -157,7 +159,16 @@ static NSString * _Nonnull stringByRemovingCharactersFromSet(NSString * _Nonnull
|
|||
}
|
||||
if (sanitizedNumber.length == binRange.length) {
|
||||
BOOL isValidLuhn = [self stringIsValidLuhn:sanitizedNumber];
|
||||
return isValidLuhn ? STPCardValidationStateValid : STPCardValidationStateInvalid;
|
||||
if (isValidLuhn) {
|
||||
if (!binRange.isCardMetadata && [STPBINRange hasBINRangesForPrefix:sanitizedNumber]) {
|
||||
// log that we didn't get a match in the metadata response so fell back to a hard coded response
|
||||
[[STPAnalyticsClient sharedClient] logCardMetadataMissingRangeWithConfiguration:[STPPaymentConfiguration sharedConfiguration]];
|
||||
}
|
||||
return STPCardValidationStateValid;
|
||||
} else {
|
||||
return STPCardValidationStateInvalid;
|
||||
}
|
||||
|
||||
} else if (sanitizedNumber.length > binRange.length) {
|
||||
return STPCardValidationStateInvalid;
|
||||
} else {
|
||||
|
@ -252,25 +263,6 @@ static NSString * _Nonnull stringByRemovingCharactersFromSet(NSString * _Nonnull
|
|||
return [[[self cardNumberFormatForBrand:brand] lastObject] unsignedIntegerValue];
|
||||
}
|
||||
|
||||
+ (BOOL)stringIsValidLuhn:(NSString *)number {
|
||||
BOOL odd = true;
|
||||
int sum = 0;
|
||||
NSMutableArray *digits = [NSMutableArray arrayWithCapacity:number.length];
|
||||
|
||||
for (int i = 0; i < (NSInteger)number.length; i++) {
|
||||
[digits addObject:[number substringWithRange:NSMakeRange(i, 1)]];
|
||||
}
|
||||
|
||||
for (NSString *digitStr in [digits reverseObjectEnumerator]) {
|
||||
int digit = [digitStr intValue];
|
||||
if ((odd = !odd)) digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
sum += digit;
|
||||
}
|
||||
|
||||
return sum % 10 == 0;
|
||||
}
|
||||
|
||||
+ (NSInteger)currentYear {
|
||||
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
|
||||
NSDateComponents *dateComponents = [calendar components:NSCalendarUnitYear fromDate:[NSDate date]];
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
#import "NSArray+Stripe.h"
|
||||
#import "NSString+Stripe.h"
|
||||
#import "STPBINRange.h"
|
||||
#import "STPCardLoadingIndicator.h"
|
||||
#import "STPCardValidator+Private.h"
|
||||
#import "STPFormTextField.h"
|
||||
#import "STPImageLibrary.h"
|
||||
|
@ -22,7 +24,7 @@
|
|||
#import "STPAnalyticsClient.h"
|
||||
|
||||
@interface STPPaymentCardTextField()<STPFormTextFieldDelegate> {
|
||||
BOOL _isNumberImageRequestValid;
|
||||
STPCardLoadingIndicator *_metadataLoadingIndicator;
|
||||
}
|
||||
|
||||
@property (nonatomic, readwrite, weak) UIImageView *brandImageView;
|
||||
|
@ -89,6 +91,7 @@ NS_INLINE CGFloat stp_ceilCGFloat(CGFloat x) {
|
|||
#endif
|
||||
}
|
||||
|
||||
static const NSTimeInterval kCardLoadingAnimationDelay = 0.1;
|
||||
|
||||
@implementation STPPaymentCardTextField
|
||||
|
||||
|
@ -686,7 +689,7 @@ CGFloat const STPPaymentCardTextFieldMinimumPadding = 10;
|
|||
|
||||
switch (fieldType) {
|
||||
case STPCardFieldTypeNumber:
|
||||
// no-op, we don't autoadvance for number so we want to leave as incomplete
|
||||
state = self.viewModel.hasCompleteMetadataForCardNumber ? [STPCardValidator validationStateForNumber:self.viewModel.cardNumber validatingCardBrand:YES] : STPCardValidationStateIncomplete;
|
||||
break;
|
||||
|
||||
case STPCardFieldTypeExpiration:
|
||||
|
@ -897,7 +900,9 @@ typedef NS_ENUM(NSInteger, STPCardTextFieldState) {
|
|||
}
|
||||
|
||||
- (CGRect)brandImageRectForBounds:(CGRect)bounds {
|
||||
return CGRectMake(STPPaymentCardTextFieldDefaultPadding, -1, self.brandImageView.image.size.width, bounds.size.height);
|
||||
CGFloat height = (CGFloat)MIN(bounds.size.height, self.brandImageView.image.size.height);
|
||||
// the -1 to y here helps the image actually be centered
|
||||
return CGRectMake(STPPaymentCardTextFieldDefaultPadding, 0.5f*bounds.size.height - 0.5f*height -1, self.brandImageView.image.size.width, height);
|
||||
}
|
||||
|
||||
- (CGRect)fieldsRectForBounds:(CGRect)bounds {
|
||||
|
@ -1318,14 +1323,44 @@ typedef void (^STPLayoutAnimationCompletionBlock)(BOOL completed);
|
|||
self.cvcField.validText = [self.viewModel validationStateForCVC] != STPCardValidationStateInvalid;
|
||||
[self updateImageForFieldType:fieldType];
|
||||
|
||||
if (self.viewModel.hasCompleteMetadataForCardNumber) {
|
||||
STPCardValidationState state = [STPCardValidator validationStateForNumber:self.viewModel.cardNumber validatingCardBrand:YES];
|
||||
[self updateCVCPlaceholder];
|
||||
self.cvcField.validText = [self.viewModel validationStateForCVC] != STPCardValidationStateInvalid;
|
||||
formTextField.validText = state != STPCardValidationStateInvalid;
|
||||
|
||||
if (state == STPCardValidationStateValid) {
|
||||
// auto-advance
|
||||
[[self nextFirstResponderField] becomeFirstResponder];
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
|
||||
}
|
||||
} else {
|
||||
[self.viewModel validationStateForCardNumberWithHandler:^(STPCardValidationState state) {
|
||||
if ([self.viewModel.cardNumber isEqualToString:number]) {
|
||||
[self updateCVCPlaceholder];
|
||||
self.cvcField.validText = [self.viewModel validationStateForCVC] != STPCardValidationStateInvalid;
|
||||
formTextField.validText = state != STPCardValidationStateInvalid;
|
||||
if (state == STPCardValidationStateValid) {
|
||||
// log that user entered full complete PAN before we got a network response
|
||||
[[STPAnalyticsClient sharedClient] logUserEnteredCompletePANBeforeMetadataLoadedWithConfiguration:[STPPaymentConfiguration sharedConfiguration]];
|
||||
}
|
||||
[self onChange];
|
||||
}
|
||||
// Update image on response because we may want to remove the loading indicator
|
||||
[self updateImageForFieldType:([self currentFirstResponderField] ?: self.numberField).tag];
|
||||
// no auto-advance
|
||||
}];
|
||||
|
||||
if (self.viewModel.isNumberMaxLength) {
|
||||
BOOL isValidLuhn = [STPCardValidator stringIsValidLuhn:self.viewModel.cardNumber];
|
||||
formTextField.validText = isValidLuhn;
|
||||
if (isValidLuhn) {
|
||||
// auto-advance
|
||||
[[self nextFirstResponderField] becomeFirstResponder];
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -1557,7 +1592,11 @@ typedef NS_ENUM(NSInteger, STPFieldEditingTransitionCallSite) {
|
|||
if (validationState == STPCardValidationStateInvalid) {
|
||||
return [self.class errorImageForCardBrand:self.viewModel.brand];
|
||||
} else {
|
||||
if (self.viewModel.hasCompleteMetadataForCardNumber) {
|
||||
return [self.class brandImageForCardBrand:self.viewModel.brand];
|
||||
} else {
|
||||
return [self.class brandImageForCardBrand:STPCardBrandUnknown];
|
||||
}
|
||||
}
|
||||
case STPCardFieldTypeCVC:
|
||||
return [self.class cvcImageForCardBrand:self.viewModel.brand];
|
||||
|
@ -1598,7 +1637,46 @@ typedef NS_ENUM(NSInteger, STPFieldEditingTransitionCallSite) {
|
|||
|
||||
- (void)updateImageForFieldType:(STPCardFieldType)fieldType {
|
||||
|
||||
void (^applyImage)(STPCardFieldType, STPCardValidationState) = ^void(STPCardFieldType applyFieldType, STPCardValidationState validationState) {
|
||||
void (^addLoadingIndicator)(void) = ^{
|
||||
if (self->_metadataLoadingIndicator == nil) {
|
||||
self->_metadataLoadingIndicator = [[STPCardLoadingIndicator alloc] init];
|
||||
|
||||
self->_metadataLoadingIndicator.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:self->_metadataLoadingIndicator];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self->_metadataLoadingIndicator.rightAnchor constraintEqualToAnchor:self.brandImageView.rightAnchor],
|
||||
[self->_metadataLoadingIndicator.topAnchor constraintEqualToAnchor:self.brandImageView.topAnchor],
|
||||
]];
|
||||
}
|
||||
|
||||
STPCardLoadingIndicator *loadingIndicator = self->_metadataLoadingIndicator;
|
||||
if (!loadingIndicator.isHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingIndicator.alpha = 0.f;
|
||||
loadingIndicator.hidden = NO;
|
||||
[UIView animateWithDuration:0.6 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
loadingIndicator.alpha = 1.f;
|
||||
} completion:^(__unused BOOL finished) {
|
||||
loadingIndicator.alpha = 1.f;
|
||||
}];
|
||||
};
|
||||
|
||||
void (^removeLoadingIndicator)(void) = ^{
|
||||
if (self->_metadataLoadingIndicator != nil && !self->_metadataLoadingIndicator.isHidden) {
|
||||
STPCardLoadingIndicator *loadingIndicator = self->_metadataLoadingIndicator;
|
||||
|
||||
[UIView animateWithDuration:0.6 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
loadingIndicator.alpha = 0.f;
|
||||
} completion:^(__unused BOOL finished) {
|
||||
loadingIndicator.alpha = 0.f;
|
||||
loadingIndicator.hidden = YES;
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
void (^applyBrandImage)(STPCardFieldType, STPCardValidationState) = ^void(STPCardFieldType applyFieldType, STPCardValidationState validationState) {
|
||||
UIImage *image = [self brandImageForFieldType:applyFieldType validationState:validationState];
|
||||
if (![image isEqual:self.brandImageView.image]) {
|
||||
|
||||
|
@ -1621,44 +1699,36 @@ typedef NS_ENUM(NSInteger, STPFieldEditingTransitionCallSite) {
|
|||
}
|
||||
};
|
||||
|
||||
if (!self.viewModel.hasCompleteMetadataForCardNumber && [STPBINRange isLoadingCardMetadataForPrefix:self.viewModel.cardNumber]) {
|
||||
applyBrandImage(STPCardFieldTypeNumber, STPCardValidationStateIncomplete);
|
||||
// delay a bit before showing loading indicator because the response may come quickly
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kCardLoadingAnimationDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (!self.viewModel.hasCompleteMetadataForCardNumber && [STPBINRange isLoadingCardMetadataForPrefix:self.viewModel.cardNumber]) {
|
||||
addLoadingIndicator();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
removeLoadingIndicator();
|
||||
|
||||
switch (fieldType) {
|
||||
|
||||
case STPCardFieldTypeNumber: {
|
||||
if (self.currentBrandImageFieldType != STPCardFieldTypeNumber) {
|
||||
STPCardValidationState cardStateForImmediateUpdate = self.numberField.validText ? STPCardValidationStateIncomplete : STPCardValidationStateInvalid;
|
||||
applyImage(fieldType, cardStateForImmediateUpdate);
|
||||
}
|
||||
STPCardFieldType currentImageFieldType = self.currentBrandImageFieldType;
|
||||
NSString *currentNumber = [self.numberField.text copy];
|
||||
_isNumberImageRequestValid = YES;
|
||||
[self.viewModel validationStateForCardNumberWithHandler:^(STPCardValidationState state) {
|
||||
if (self->_isNumberImageRequestValid &&
|
||||
currentImageFieldType == self.currentBrandImageFieldType &&
|
||||
([currentNumber isEqualToString:self.numberField.text] || (currentNumber == nil && self.numberField.text == nil))) {
|
||||
// we haven't requested image for another field and the card number has not changed. Update with this state
|
||||
applyImage(fieldType, state);
|
||||
self->_isNumberImageRequestValid = NO;
|
||||
}
|
||||
}];
|
||||
}
|
||||
case STPCardFieldTypeNumber:
|
||||
applyBrandImage(STPCardFieldTypeNumber, [STPCardValidator validationStateForNumber:self.viewModel.cardNumber validatingCardBrand:YES]);
|
||||
break;
|
||||
|
||||
case STPCardFieldTypeExpiration:
|
||||
_isNumberImageRequestValid = NO;
|
||||
applyImage(fieldType, [self.viewModel validationStateForExpiration]);
|
||||
applyBrandImage(fieldType, [self.viewModel validationStateForExpiration]);
|
||||
break;
|
||||
|
||||
case STPCardFieldTypeCVC:
|
||||
_isNumberImageRequestValid = NO;
|
||||
applyImage(fieldType, [self.viewModel validationStateForCVC]);
|
||||
applyBrandImage(fieldType, [self.viewModel validationStateForCVC]);
|
||||
break;
|
||||
|
||||
case STPCardFieldTypePostalCode:
|
||||
_isNumberImageRequestValid = NO;
|
||||
applyImage(fieldType, [self.viewModel validationStateForPostalCode]);
|
||||
applyBrandImage(fieldType, [self.viewModel validationStateForPostalCode]);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)defaultCVCPlaceholder {
|
||||
|
@ -1702,10 +1772,17 @@ typedef NS_ENUM(NSInteger, STPFieldEditingTransitionCallSite) {
|
|||
|
||||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingIsValid {
|
||||
return [NSSet setWithArray:@[
|
||||
// viewModel.valid
|
||||
[NSString stringWithFormat:@"%@.%@",
|
||||
NSStringFromSelector(@selector(viewModel)),
|
||||
NSStringFromSelector(@selector(valid))],
|
||||
]];
|
||||
|
||||
// viewModel.hasCompleteMetadataForCardNumber
|
||||
[NSString stringWithFormat:@"%@.%@",
|
||||
NSStringFromSelector(@selector(viewModel)),
|
||||
NSStringFromSelector(@selector(hasCompleteMetadataForCardNumber))],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -35,6 +35,8 @@ typedef NS_ENUM(NSInteger, STPCardFieldType) {
|
|||
@property (nonatomic, copy, nullable) NSString *postalCodeCountryCode;
|
||||
@property (nonatomic, readonly) STPCardBrand brand;
|
||||
@property (nonatomic, readonly) BOOL isValid;
|
||||
@property (nonatomic, readonly) BOOL hasCompleteMetadataForCardNumber;
|
||||
@property (nonatomic, readonly) BOOL isNumberMaxLength;
|
||||
|
||||
- (NSString *)defaultPlaceholder;
|
||||
- (nullable NSString *)compressedCardNumberWithPlaceholder:(nullable NSString *)placeholder;
|
||||
|
|
|
@ -13,10 +13,24 @@
|
|||
#import "STPCardValidator+Private.h"
|
||||
#import "STPPostalCodeValidator.h"
|
||||
|
||||
@interface STPPaymentCardTextFieldViewModel ()
|
||||
|
||||
@property (nonatomic, readwrite) BOOL hasCompleteMetadataForCardNumber;
|
||||
|
||||
@end
|
||||
|
||||
@implementation STPPaymentCardTextFieldViewModel
|
||||
|
||||
- (void)setCardNumber:(NSString *)cardNumber {
|
||||
_cardNumber = [STPCardValidator sanitizedNumericStringForString:cardNumber];
|
||||
NSString *sanitizedNumber = [STPCardValidator sanitizedNumericStringForString:cardNumber];
|
||||
self.hasCompleteMetadataForCardNumber = [STPBINRange hasBINRangesForPrefix:sanitizedNumber];
|
||||
if (self.hasCompleteMetadataForCardNumber) {
|
||||
STPCardBrand brand = [STPCardValidator brandForNumber:sanitizedNumber];
|
||||
NSInteger maxLength = [STPCardValidator maxLengthForCardBrand:brand];
|
||||
_cardNumber = [sanitizedNumber stp_safeSubstringToIndex:maxLength];
|
||||
} else {
|
||||
_cardNumber = [sanitizedNumber stp_safeSubstringToIndex:[STPBINRange maxCardNumberLength]];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSString *)compressedCardNumberWithPlaceholder:(nullable NSString *)placeholder {
|
||||
|
@ -116,6 +130,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (BOOL)isNumberMaxLength {
|
||||
return self.cardNumber.length == [STPBINRange maxCardNumberLength];
|
||||
}
|
||||
|
||||
- (STPCardValidationState)validationStateForPostalCode {
|
||||
if (self.postalCode.length > 0) {
|
||||
return STPCardValidationStateValid;
|
||||
|
@ -126,12 +144,14 @@
|
|||
|
||||
- (void)validationStateForCardNumberWithHandler:(void (^)(STPCardValidationState))handler {
|
||||
[STPBINRange retrieveBINRangesForPrefix:self.cardNumber completion:^(__unused NSArray<STPBINRange *> * _Nullable ranges, __unused NSError * _Nullable error) {
|
||||
self.hasCompleteMetadataForCardNumber = [STPBINRange hasBINRangesForPrefix:self.cardNumber];
|
||||
handler([STPCardValidator validationStateForNumber:self.cardNumber validatingCardBrand:YES]);
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)isValid {
|
||||
return ([STPCardValidator validationStateForNumber:self.cardNumber validatingCardBrand:YES] == STPCardValidationStateValid
|
||||
&& self.hasCompleteMetadataForCardNumber
|
||||
&& [self validationStateForExpiration] == STPCardValidationStateValid
|
||||
&& [self validationStateForCVC] == STPCardValidationStateValid
|
||||
&& (!self.postalCodeRequired
|
||||
|
@ -156,6 +176,7 @@
|
|||
NSStringFromSelector(@selector(postalCode)),
|
||||
NSStringFromSelector(@selector(postalCodeRequested)),
|
||||
NSStringFromSelector(@selector(postalCodeCountryCode)),
|
||||
NSStringFromSelector(@selector(hasCompleteMetadataForCardNumber)),
|
||||
]];
|
||||
}
|
||||
|
||||
|
|
|
@ -114,8 +114,10 @@
|
|||
}
|
||||
|
||||
XCTAssertEqual(STPCardValidationStateIncomplete, [STPCardValidator validationStateForNumber:@"1" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateValid, [STPCardValidator validationStateForNumber:@"0000000000000000" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateValid, [STPCardValidator validationStateForNumber:@"9999999999999995" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateIncomplete, [STPCardValidator validationStateForNumber:@"0000000000000000" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateIncomplete, [STPCardValidator validationStateForNumber:@"9999999999999995" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateValid, [STPCardValidator validationStateForNumber:@"0000000000000000000" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateValid, [STPCardValidator validationStateForNumber:@"9999999999999999998" validatingCardBrand:NO]);
|
||||
XCTAssertEqual(STPCardValidationStateIncomplete, [STPCardValidator validationStateForNumber:@"4242424242424" validatingCardBrand:YES]);
|
||||
XCTAssertEqual(STPCardValidationStateIncomplete, [STPCardValidator validationStateForNumber:nil validatingCardBrand:YES]);
|
||||
}
|
||||
|
@ -135,7 +137,7 @@
|
|||
@[@(STPCardBrandDinersClub), @[@14, @16]],
|
||||
@[@(STPCardBrandJCB), @[@16]],
|
||||
@[@(STPCardBrandUnionPay), @[@16]],
|
||||
@[@(STPCardBrandUnknown), @[@16]],
|
||||
@[@(STPCardBrandUnknown), @[@19]],
|
||||
];
|
||||
for (NSArray *test in tests) {
|
||||
NSSet *lengths = [STPCardValidator lengthsForCardBrand:[test[0] integerValue]];
|
||||
|
|
|
@ -64,6 +64,11 @@
|
|||
// because we are calling to the card metadata service without configuration STPAPIClient
|
||||
@implementation STPPaymentCardTextFieldTest
|
||||
|
||||
+ (void)setUp {
|
||||
[super setUp];
|
||||
[[STPAPIClient sharedClient] setPublishableKey:STPTestingDefaultPublishableKey];
|
||||
}
|
||||
|
||||
- (void)testIntrinsicContentSize {
|
||||
STPPaymentCardTextField *textField = [STPPaymentCardTextField new];
|
||||
|
||||
|
@ -93,10 +98,6 @@
|
|||
card.number = number;
|
||||
[sut setCardParams:card];
|
||||
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField errorImageForCardBrand:STPCardBrandUnknown]);
|
||||
|
||||
|
@ -107,10 +108,6 @@
|
|||
XCTAssertEqual(sut.expirationField.text.length, (NSUInteger)0);
|
||||
XCTAssertEqual(sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_expiration {
|
||||
|
@ -164,13 +161,10 @@
|
|||
- (void)testSetCard_numberVisa {
|
||||
STPPaymentCardTextField *sut = [STPPaymentCardTextField new];
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
NSString *number = @"4242";
|
||||
NSString *number = @"424242";
|
||||
card.number = number;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
|
@ -183,11 +177,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.placeholder, @"CVC");
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_numberVisaInvalid {
|
||||
|
@ -197,31 +186,19 @@
|
|||
card.number = number;
|
||||
[sut setCardParams:card];
|
||||
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField errorImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_numberAmex {
|
||||
STPPaymentCardTextField *sut = [STPPaymentCardTextField new];
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
NSString *number = @"3782";
|
||||
NSString *number = @"378282";
|
||||
card.number = number;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandAmex]);
|
||||
|
||||
|
@ -233,10 +210,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.placeholder, @"CVV");
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_numberAmexInvalid {
|
||||
|
@ -245,20 +218,13 @@
|
|||
NSString *number = @"378282246311111";
|
||||
card.number = number;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField errorImageForCardBrand:STPCardBrandAmex]);
|
||||
|
||||
XCTAssertNotNil(sut.focusedTextFieldForLayout);
|
||||
XCTAssertTrue(sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeNumber);
|
||||
XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_numberAndExpiration {
|
||||
|
@ -269,10 +235,7 @@
|
|||
card.expMonth = @(10);
|
||||
card.expYear = @(99);
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
|
@ -283,24 +246,17 @@
|
|||
XCTAssertEqual(sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_partialNumberAndExpiration {
|
||||
STPPaymentCardTextField *sut = [STPPaymentCardTextField new];
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
NSString *number = @"42";
|
||||
NSString *number = @"424242";
|
||||
card.number = number;
|
||||
card.expMonth = @(10);
|
||||
card.expYear = @(99);
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
|
@ -312,11 +268,6 @@
|
|||
XCTAssertEqual(sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_numberAndCVC {
|
||||
|
@ -327,10 +278,7 @@
|
|||
card.number = number;
|
||||
card.cvc = cvc;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandAmex]);
|
||||
|
||||
|
@ -341,11 +289,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.text, cvc);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_expirationAndCVC {
|
||||
|
@ -356,10 +299,7 @@
|
|||
card.expYear = @(99);
|
||||
card.cvc = cvc;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandUnknown]);
|
||||
|
||||
|
@ -371,11 +311,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.text, cvc);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_completeCardCountryWithoutPostal {
|
||||
|
@ -389,10 +324,7 @@
|
|||
card.expYear = @(99);
|
||||
card.cvc = cvc;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
|
@ -403,11 +335,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.text, cvc);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertTrue(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_completeCardNoPostal {
|
||||
|
@ -421,10 +348,7 @@
|
|||
card.expYear = @(99);
|
||||
card.cvc = cvc;
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
|
@ -435,11 +359,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.text, cvc);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertTrue(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_completeCard {
|
||||
|
@ -453,10 +372,7 @@
|
|||
card.cvc = cvc;
|
||||
sut.postalCodeField.text = @"90210";
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
|
@ -467,11 +383,6 @@
|
|||
XCTAssertEqualObjects(sut.cvcField.text, cvc);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertTrue(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testSetCard_empty {
|
||||
|
@ -481,10 +392,7 @@
|
|||
sut.expirationField.text = @"10/99";
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
[sut setCardParams:card];
|
||||
// The view model needs to request card metadata to choose the correct image, so give it
|
||||
// time for a network roundtrip
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandUnknown]);
|
||||
|
||||
|
@ -496,11 +404,6 @@
|
|||
XCTAssertEqual(sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertNil(sut.currentFirstResponderField);
|
||||
XCTAssertFalse(sut.isValid);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
@ -585,333 +488,299 @@
|
|||
|
||||
@implementation STPPaymentCardTextFieldUITests
|
||||
|
||||
//- (void)setUp {
|
||||
// [super setUp];
|
||||
// self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
// STPPaymentCardTextField *textField = [[STPPaymentCardTextField alloc] initWithFrame:self.window.bounds];
|
||||
// [self.window addSubview:textField];
|
||||
// XCTAssertTrue([textField.numberField canBecomeFirstResponder], @"text field cannot become first responder");
|
||||
// self.sut = textField;
|
||||
//}
|
||||
//
|
||||
//#pragma mark - UI Tests
|
||||
//
|
||||
//- (void)testSetCard_allFields_whileEditingNumber {
|
||||
// XCTAssertTrue([self.sut.numberField becomeFirstResponder], @"text field is not first responder");
|
||||
// STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
// self.sut.postalCodeField.text = @"90210";
|
||||
// NSString *number = @"4242424242424242";
|
||||
// NSString *cvc = @"123";
|
||||
// card.number = number;
|
||||
// card.expMonth = @(10);
|
||||
// card.expYear = @(99);
|
||||
// card.cvc = cvc;
|
||||
// [self.sut setCardParams:card];
|
||||
//
|
||||
// // The view model needs to request card metadata to choose the correct image, so give it
|
||||
// // time for a network roundtrip
|
||||
// XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
// NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
// NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
//
|
||||
// XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
// XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
// XCTAssertEqualObjects(self.sut.numberField.text, number);
|
||||
// XCTAssertEqualObjects(self.sut.expirationField.text, @"10/99");
|
||||
// XCTAssertEqualObjects(self.sut.cvcField.text, cvc);
|
||||
// XCTAssertEqualObjects(self.sut.postalCode, @"90210");
|
||||
// XCTAssertTrue([self.sut isFirstResponder], @"after `setCardParams:`, should still be first responder to allow number editing");
|
||||
// XCTAssertTrue(self.sut.isValid);
|
||||
//
|
||||
// [expectation fulfill];
|
||||
// });
|
||||
//
|
||||
// [self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
//}
|
||||
//
|
||||
//- (void)testSetCard_partialNumberAndExpiration_whileEditingExpiration {
|
||||
// XCTAssertTrue([self.sut.expirationField becomeFirstResponder], @"text field is not first responder");
|
||||
// STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
// NSString *number = @"42";
|
||||
// card.number = number;
|
||||
// card.expMonth = @(10);
|
||||
// card.expYear = @(99);
|
||||
// [self.sut setCardParams:card];
|
||||
//
|
||||
// // The view model needs to request card metadata to choose the correct image, so give it
|
||||
// // time for a network roundtrip
|
||||
// XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
// NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
// NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField cvcImageForCardBrand:STPCardBrandVisa]);
|
||||
//
|
||||
// XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
// XCTAssertTrue(self.sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeCVC);
|
||||
// XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
// XCTAssertEqualObjects(self.sut.numberField.text, number);
|
||||
// XCTAssertEqualObjects(self.sut.expirationField.text, @"10/99");
|
||||
// XCTAssertEqual(self.sut.cvcField.text.length, (NSUInteger)0);
|
||||
// XCTAssertTrue([self.sut.cvcField isFirstResponder], @"after `setCardParams:`, when firstResponder becomes valid, first invalid field should become firstResponder");
|
||||
// XCTAssertFalse(self.sut.isValid);
|
||||
//
|
||||
// [expectation fulfill];
|
||||
// });
|
||||
//
|
||||
// [self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
//}
|
||||
//
|
||||
//- (void)testSetCard_number_whileEditingCVC {
|
||||
// XCTAssertTrue([self.sut.cvcField becomeFirstResponder], @"text field is not first responder");
|
||||
// STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
// NSString *number = @"4242424242424242";
|
||||
// card.number = number;
|
||||
// [self.sut setCardParams:card];
|
||||
//
|
||||
// // The view model needs to request card metadata to choose the correct image, so give it
|
||||
// // time for a network roundtrip
|
||||
// XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
// NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
// NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField cvcImageForCardBrand:STPCardBrandVisa]);
|
||||
//
|
||||
// XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
// XCTAssertTrue(self.sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeCVC);
|
||||
// XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
// XCTAssertEqualObjects(self.sut.numberField.text, number);
|
||||
// XCTAssertEqual(self.sut.expirationField.text.length, (NSUInteger)0);
|
||||
// XCTAssertEqual(self.sut.cvcField.text.length, (NSUInteger)0);
|
||||
// XCTAssertTrue([self.sut.cvcField isFirstResponder], @"after `setCardParams:`, if firstResponder is invalid, it should remain firstResponder");
|
||||
// XCTAssertFalse(self.sut.isValid);
|
||||
//
|
||||
// [expectation fulfill];
|
||||
// });
|
||||
//
|
||||
// [self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
//}
|
||||
//
|
||||
//- (void)testSetCard_empty_whileEditingNumber {
|
||||
// self.sut.numberField.text = @"4242424242424242";
|
||||
// self.sut.cvcField.text = @"123";
|
||||
// self.sut.expirationField.text = @"10/99";
|
||||
// XCTAssertTrue([self.sut.numberField becomeFirstResponder], @"text field is not first responder");
|
||||
// STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
// [self.sut setCardParams:card];
|
||||
//
|
||||
// // The view model needs to request card metadata to choose the correct image, so give it
|
||||
// // time for a network roundtrip
|
||||
// XCTestExpectation *expectation = [self expectationWithDescription:@"Image fetching"];
|
||||
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(STPTestingNetworkRequestTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
// NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
// NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandUnknown]);
|
||||
//
|
||||
// XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
// XCTAssertTrue(self.sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeNumber);
|
||||
// XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
// XCTAssertEqual(self.sut.numberField.text.length, (NSUInteger)0);
|
||||
// XCTAssertEqual(self.sut.expirationField.text.length, (NSUInteger)0);
|
||||
// XCTAssertEqual(self.sut.cvcField.text.length, (NSUInteger)0);
|
||||
// XCTAssertTrue([self.sut.numberField isFirstResponder], @"after `setCardParams:` that clears the text fields, the first invalid field should become firstResponder");
|
||||
// XCTAssertFalse(self.sut.isValid);
|
||||
//
|
||||
// [expectation fulfill];
|
||||
// });
|
||||
//
|
||||
// [self waitForExpectationsWithTimeout:2*STPTestingNetworkRequestTimeout handler:nil];
|
||||
//}
|
||||
//
|
||||
//- (void)testIsValidKVO {
|
||||
// id observer = OCMClassMock([UIViewController class]);
|
||||
// self.sut.numberField.text = @"4242424242424242";
|
||||
// self.sut.expirationField.text = @"10/99";
|
||||
// self.sut.postalCodeField.text = @"90210";
|
||||
// XCTAssertFalse(self.sut.isValid);
|
||||
//
|
||||
// NSString *expectedKeyPath = @"sut.isValid";
|
||||
// [self addObserver:observer forKeyPath:expectedKeyPath options:NSKeyValueObservingOptionNew context:nil];
|
||||
// XCTestExpectation *exp = [self expectationWithDescription:@"observeValue"];
|
||||
// OCMStub([observer observeValueForKeyPath:[OCMArg any] ofObject:[OCMArg any] change:[OCMArg any] context:nil])
|
||||
// .andDo(^(NSInvocation *invocation) {
|
||||
// NSString *keyPath;
|
||||
// NSDictionary *change;
|
||||
// [invocation getArgument:&keyPath atIndex:2];
|
||||
// [invocation getArgument:&change atIndex:4];
|
||||
// if ([keyPath isEqualToString:expectedKeyPath]) {
|
||||
// if ([change[@"new"] boolValue]) {
|
||||
// [exp fulfill];
|
||||
// [self removeObserver:observer forKeyPath:@"sut.isValid"];
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// self.sut.cvcField.text = @"123";
|
||||
//
|
||||
// [self waitForExpectationsWithTimeout:2 handler:nil];
|
||||
//}
|
||||
//
|
||||
//- (void)testBecomeFirstResponder {
|
||||
// self.sut.postalCodeEntryEnabled = NO;
|
||||
// XCTAssertTrue([self.sut canBecomeFirstResponder]);
|
||||
// XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
// XCTAssertTrue(self.sut.isFirstResponder);
|
||||
//
|
||||
// XCTAssertEqual(self.sut.numberField, self.sut.currentFirstResponderField);
|
||||
//
|
||||
// [self.sut becomeFirstResponder];
|
||||
// XCTAssertEqual(self.sut.numberField, self.sut.currentFirstResponderField,
|
||||
// @"Repeated calls to becomeFirstResponder should not change the firstResponder");
|
||||
//
|
||||
// self.sut.numberField.text = @"4242" "4242" "4242" "4242";
|
||||
//
|
||||
// XCTAssertEqual(self.sut.numberField, self.sut.currentFirstResponderField,
|
||||
// @"Should not auto-advance from number field");
|
||||
//
|
||||
// XCTAssertTrue([self.sut.cvcField becomeFirstResponder]);
|
||||
// XCTAssertEqual(self.sut.cvcField, self.sut.currentFirstResponderField,
|
||||
// @"We don't block other fields from becoming firstResponder");
|
||||
//
|
||||
// XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
// XCTAssertEqual(self.sut.cvcField, self.sut.currentFirstResponderField,
|
||||
// @"Calling becomeFirstResponder does not change the currentFirstResponder");
|
||||
//
|
||||
// self.sut.expirationField.text = @"10/99";
|
||||
// self.sut.cvcField.text = @"123";
|
||||
//
|
||||
// XCTAssertTrue(self.sut.isValid);
|
||||
// [self.sut resignFirstResponder];
|
||||
// XCTAssertTrue([self.sut canBecomeFirstResponder]);
|
||||
// XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
//
|
||||
// XCTAssertEqual(self.sut.cvcField, self.sut.currentFirstResponderField,
|
||||
// @"When all fields are valid, the last one should be the preferred firstResponder");
|
||||
//
|
||||
// self.sut.postalCodeEntryEnabled = YES;
|
||||
// XCTAssertFalse(self.sut.isValid);
|
||||
//
|
||||
// [self.sut resignFirstResponder];
|
||||
// XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
// XCTAssertEqual(self.sut.postalCodeField, self.sut.currentFirstResponderField,
|
||||
// @"When postalCodeEntryEnabled=YES, it should become firstResponder after other fields are valid");
|
||||
//
|
||||
// self.sut.expirationField.text = @"";
|
||||
// [self.sut resignFirstResponder];
|
||||
// XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
// XCTAssertEqual(self.sut.expirationField, self.sut.currentFirstResponderField,
|
||||
// @"Moves firstResponder back to expiration, because it's not valid anymore");
|
||||
//
|
||||
// self.sut.expirationField.text = @"10/99";
|
||||
// self.sut.postalCodeField.text = @"90210";
|
||||
//
|
||||
// XCTAssertTrue(self.sut.isValid);
|
||||
// [self.sut resignFirstResponder];
|
||||
// XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
// XCTAssertEqual(self.sut.postalCodeField, self.sut.currentFirstResponderField,
|
||||
// @"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.postalCodeField.isFirstResponder, @"with side effect to move 1st responder to next field");
|
||||
//
|
||||
// XCTAssertFalse([self.sut.postalCodeField.delegate textFieldShouldReturn:self.sut.postalCodeField], @"shouldReturn = NO");
|
||||
// XCTAssertTrue(self.sut.numberField.isFirstResponder, @"with side effect to move 1st responder from last field to first invalid field");
|
||||
//}
|
||||
//
|
||||
//- (void)testShouldReturnCyclesThroughFieldsWithoutPostal {
|
||||
// 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.postalCodeEntryEnabled = NO;
|
||||
//
|
||||
// [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)testShouldReturnDismissesWhenValidNoPostalCode {
|
||||
// __block BOOL hasReturned = NO;
|
||||
// __block BOOL didEnd = NO;
|
||||
//
|
||||
// self.sut.postalCodeEntryEnabled = NO;
|
||||
// [self.sut setCardParams:[STPFixtures paymentMethodCardParams]];
|
||||
//
|
||||
// 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");
|
||||
//}
|
||||
//
|
||||
//- (void)testShouldReturnDismissesWhenValid {
|
||||
// __block BOOL hasReturned = NO;
|
||||
// __block BOOL didEnd = NO;
|
||||
//
|
||||
// [self.sut setCardParams:[STPFixtures paymentMethodCardParams]];
|
||||
// self.sut.postalCodeField.text = @"90210";
|
||||
// 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.postalCodeField.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.postalCodeField.delegate textFieldShouldReturn:self.sut.postalCodeField], @"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");
|
||||
//}
|
||||
+ (void)setUp {
|
||||
[super setUp];
|
||||
[[STPAPIClient sharedClient] setPublishableKey:STPTestingDefaultPublishableKey];
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
STPPaymentCardTextField *textField = [[STPPaymentCardTextField alloc] initWithFrame:self.window.bounds];
|
||||
[self.window addSubview:textField];
|
||||
XCTAssertTrue([textField.numberField canBecomeFirstResponder], @"text field cannot become first responder");
|
||||
self.sut = textField;
|
||||
}
|
||||
|
||||
#pragma mark - UI Tests
|
||||
|
||||
- (void)testSetCard_allFields_whileEditingNumber {
|
||||
XCTAssertTrue([self.sut.numberField becomeFirstResponder], @"text field is not first responder");
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
self.sut.postalCodeField.text = @"90210";
|
||||
NSString *number = @"4242424242424242";
|
||||
NSString *cvc = @"123";
|
||||
card.number = number;
|
||||
card.expMonth = @(10);
|
||||
card.expYear = @(99);
|
||||
card.cvc = cvc;
|
||||
[self.sut setCardParams:card];
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
XCTAssertNil(self.sut.focusedTextFieldForLayout);
|
||||
XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
XCTAssertEqualObjects(self.sut.numberField.text, number);
|
||||
XCTAssertEqualObjects(self.sut.expirationField.text, @"10/99");
|
||||
XCTAssertEqualObjects(self.sut.cvcField.text, cvc);
|
||||
XCTAssertEqualObjects(self.sut.postalCode, @"90210");
|
||||
XCTAssertFalse([self.sut isFirstResponder], @"after `setCardParams:`, if all fields are valid, should resign firstResponder");
|
||||
XCTAssertTrue(self.sut.isValid);
|
||||
}
|
||||
|
||||
- (void)testSetCard_partialNumberAndExpiration_whileEditingExpiration {
|
||||
XCTAssertTrue([self.sut.expirationField becomeFirstResponder], @"text field is not first responder");
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
NSString *number = @"42";
|
||||
card.number = number;
|
||||
card.expMonth = @(10);
|
||||
card.expYear = @(99);
|
||||
[self.sut setCardParams:card];
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField cvcImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
XCTAssertTrue(self.sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeCVC);
|
||||
XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
XCTAssertEqualObjects(self.sut.numberField.text, number);
|
||||
XCTAssertEqualObjects(self.sut.expirationField.text, @"10/99");
|
||||
XCTAssertEqual(self.sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertTrue([self.sut.cvcField isFirstResponder], @"after `setCardParams:`, when firstResponder becomes valid, first invalid field should become firstResponder");
|
||||
XCTAssertFalse(self.sut.isValid);
|
||||
}
|
||||
|
||||
- (void)testSetCard_number_whileEditingCVC {
|
||||
XCTAssertTrue([self.sut.cvcField becomeFirstResponder], @"text field is not first responder");
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
NSString *number = @"4242424242424242";
|
||||
card.number = number;
|
||||
[self.sut setCardParams:card];
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField cvcImageForCardBrand:STPCardBrandVisa]);
|
||||
|
||||
XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
XCTAssertTrue(self.sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeCVC);
|
||||
XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
XCTAssertEqualObjects(self.sut.numberField.text, number);
|
||||
XCTAssertEqual(self.sut.expirationField.text.length, (NSUInteger)0);
|
||||
XCTAssertEqual(self.sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertTrue([self.sut.cvcField isFirstResponder], @"after `setCardParams:`, if firstResponder is invalid, it should remain firstResponder");
|
||||
XCTAssertFalse(self.sut.isValid);
|
||||
}
|
||||
|
||||
- (void)testSetCard_empty_whileEditingNumber {
|
||||
self.sut.numberField.text = @"4242424242424242";
|
||||
self.sut.cvcField.text = @"123";
|
||||
self.sut.expirationField.text = @"10/99";
|
||||
XCTAssertTrue([self.sut.numberField becomeFirstResponder], @"text field is not first responder");
|
||||
STPPaymentMethodCardParams *card = [STPPaymentMethodCardParams new];
|
||||
[self.sut setCardParams:card];
|
||||
|
||||
NSData *imgData = UIImagePNGRepresentation(self.sut.brandImageView.image);
|
||||
NSData *expectedImgData = UIImagePNGRepresentation([STPPaymentCardTextField brandImageForCardBrand:STPCardBrandUnknown]);
|
||||
|
||||
XCTAssertNotNil(self.sut.focusedTextFieldForLayout);
|
||||
XCTAssertTrue(self.sut.focusedTextFieldForLayout.integerValue == STPCardFieldTypeNumber);
|
||||
XCTAssertTrue([expectedImgData isEqualToData:imgData]);
|
||||
XCTAssertEqual(self.sut.numberField.text.length, (NSUInteger)0);
|
||||
XCTAssertEqual(self.sut.expirationField.text.length, (NSUInteger)0);
|
||||
XCTAssertEqual(self.sut.cvcField.text.length, (NSUInteger)0);
|
||||
XCTAssertTrue([self.sut.numberField isFirstResponder], @"after `setCardParams:` that clears the text fields, the first invalid field should become firstResponder");
|
||||
XCTAssertFalse(self.sut.isValid);
|
||||
}
|
||||
|
||||
- (void)testIsValidKVO {
|
||||
id observer = OCMClassMock([UIViewController class]);
|
||||
self.sut.numberField.text = @"4242424242424242";
|
||||
self.sut.expirationField.text = @"10/99";
|
||||
self.sut.postalCodeField.text = @"90210";
|
||||
XCTAssertFalse(self.sut.isValid);
|
||||
|
||||
NSString *expectedKeyPath = @"sut.isValid";
|
||||
[self addObserver:observer forKeyPath:expectedKeyPath options:NSKeyValueObservingOptionNew context:nil];
|
||||
XCTestExpectation *exp = [self expectationWithDescription:@"observeValue"];
|
||||
OCMStub([observer observeValueForKeyPath:[OCMArg any] ofObject:[OCMArg any] change:[OCMArg any] context:nil])
|
||||
.andDo(^(NSInvocation *invocation) {
|
||||
NSString *keyPath;
|
||||
NSDictionary *change;
|
||||
[invocation getArgument:&keyPath atIndex:2];
|
||||
[invocation getArgument:&change atIndex:4];
|
||||
if ([keyPath isEqualToString:expectedKeyPath]) {
|
||||
if ([change[@"new"] boolValue]) {
|
||||
[exp fulfill];
|
||||
[self removeObserver:observer forKeyPath:@"sut.isValid"];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.sut.cvcField.text = @"123";
|
||||
|
||||
[self waitForExpectationsWithTimeout:STPTestingNetworkRequestTimeout handler:nil];
|
||||
}
|
||||
|
||||
- (void)testBecomeFirstResponder {
|
||||
self.sut.postalCodeEntryEnabled = NO;
|
||||
XCTAssertTrue([self.sut canBecomeFirstResponder]);
|
||||
XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
XCTAssertTrue(self.sut.isFirstResponder);
|
||||
|
||||
XCTAssertEqual(self.sut.numberField, self.sut.currentFirstResponderField);
|
||||
|
||||
[self.sut becomeFirstResponder];
|
||||
XCTAssertEqual(self.sut.numberField, self.sut.currentFirstResponderField,
|
||||
@"Repeated calls to becomeFirstResponder should not change the firstResponder");
|
||||
|
||||
self.sut.numberField.text = @"4242" "4242" "4242" "4242";
|
||||
|
||||
// Don't unit test auto-advance from number field here because we don't know the cache state
|
||||
|
||||
XCTAssertTrue([self.sut.cvcField becomeFirstResponder]);
|
||||
XCTAssertEqual(self.sut.cvcField, self.sut.currentFirstResponderField,
|
||||
@"We don't block other fields from becoming firstResponder");
|
||||
|
||||
XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
XCTAssertEqual(self.sut.cvcField, self.sut.currentFirstResponderField,
|
||||
@"Calling becomeFirstResponder does not change the currentFirstResponder");
|
||||
|
||||
self.sut.expirationField.text = @"10/99";
|
||||
self.sut.cvcField.text = @"123";
|
||||
|
||||
[self.sut resignFirstResponder];
|
||||
XCTAssertTrue([self.sut canBecomeFirstResponder]);
|
||||
XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
|
||||
XCTAssertEqual(self.sut.cvcField, self.sut.currentFirstResponderField,
|
||||
@"When all fields are valid, the last one should be the preferred firstResponder");
|
||||
|
||||
self.sut.postalCodeEntryEnabled = YES;
|
||||
XCTAssertFalse(self.sut.isValid);
|
||||
|
||||
[self.sut resignFirstResponder];
|
||||
XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
XCTAssertEqual(self.sut.postalCodeField, self.sut.currentFirstResponderField,
|
||||
@"When postalCodeEntryEnabled=YES, it should become firstResponder after other fields are valid");
|
||||
|
||||
self.sut.expirationField.text = @"";
|
||||
[self.sut resignFirstResponder];
|
||||
XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
XCTAssertEqual(self.sut.expirationField, self.sut.currentFirstResponderField,
|
||||
@"Moves firstResponder back to expiration, because it's not valid anymore");
|
||||
|
||||
self.sut.expirationField.text = @"10/99";
|
||||
self.sut.postalCodeField.text = @"90210";
|
||||
|
||||
[self.sut resignFirstResponder];
|
||||
XCTAssertTrue([self.sut becomeFirstResponder]);
|
||||
XCTAssertEqual(self.sut.postalCodeField, self.sut.currentFirstResponderField,
|
||||
@"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.postalCodeField.isFirstResponder, @"with side effect to move 1st responder to next field");
|
||||
|
||||
XCTAssertFalse([self.sut.postalCodeField.delegate textFieldShouldReturn:self.sut.postalCodeField], @"shouldReturn = NO");
|
||||
XCTAssertTrue(self.sut.numberField.isFirstResponder, @"with side effect to move 1st responder from last field to first invalid field");
|
||||
}
|
||||
|
||||
- (void)testShouldReturnCyclesThroughFieldsWithoutPostal {
|
||||
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.postalCodeEntryEnabled = NO;
|
||||
|
||||
[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)testShouldReturnDismissesWhenValidNoPostalCode {
|
||||
__block BOOL hasReturned = NO;
|
||||
__block BOOL didEnd = NO;
|
||||
|
||||
self.sut.postalCodeEntryEnabled = NO;
|
||||
[self.sut setCardParams:[STPFixtures paymentMethodCardParams]];
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
- (void)testShouldReturnDismissesWhenValid {
|
||||
__block BOOL hasReturned = NO;
|
||||
__block BOOL didEnd = NO;
|
||||
|
||||
[self.sut setCardParams:[STPFixtures paymentMethodCardParams]];
|
||||
self.sut.postalCodeField.text = @"90210";
|
||||
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.postalCodeField.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.postalCodeField.delegate textFieldShouldReturn:self.sut.postalCodeField], @"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
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
@[@"4242424242424242", @"4242424242424242"],
|
||||
@[@"4242 4242 4242 4242", @"4242424242424242"],
|
||||
@[@"4242xxx4242", @"42424242"],
|
||||
@[@"12345678901234567890", @"12345678901234567890"],
|
||||
@[@"12345678901234567890", @"1234567890123456789"],
|
||||
];
|
||||
for (NSArray *test in tests) {
|
||||
self.viewModel.cardNumber = test[0];
|
||||
|
|
Loading…
Reference in New Issue