diff --git a/Stripe.podspec b/Stripe.podspec index 4507be8654..1bcd53e453 100644 --- a/Stripe.podspec +++ b/Stripe.podspec @@ -7,6 +7,7 @@ Pod::Spec.new do |s| s.authors = { 'Jack Flintermann' => 'jack@stripe.com', 'Stripe' => 'support+github@stripe.com' } s.source = { :git => 'https://github.com/stripe/stripe-ios.git', :tag => "v#{s.version}" } s.frameworks = 'Foundation', 'Security', 'WebKit', 'PassKit', 'AddressBook' + s.weak_frameworks = 'Contacts' s.requires_arc = true s.platform = :ios s.ios.deployment_target = '8.0' diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj index 1ecbee905b..3028768988 100644 --- a/Stripe.xcodeproj/project.pbxproj +++ b/Stripe.xcodeproj/project.pbxproj @@ -596,6 +596,7 @@ F1510BBF1D5A8146000731AD /* stp_card_jcb_template.png in Resources */ = {isa = PBXBuildFile; fileRef = F1510BBC1D5A8146000731AD /* stp_card_jcb_template.png */; }; F1510BC21D5A8146000731AD /* stp_card_jcb_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F1510BBD1D5A8146000731AD /* stp_card_jcb_template@2x.png */; }; F1510BC51D5A8146000731AD /* stp_card_jcb_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F1510BBE1D5A8146000731AD /* stp_card_jcb_template@3x.png */; }; + F15232311EA93E6800D65C67 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F15232301EA93E6800D65C67 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; F15675401DB544D3004468E3 /* STPAddCardViewControllerLocalizationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F156753F1DB544D3004468E3 /* STPAddCardViewControllerLocalizationTests.m */; }; F15AC18E1DBA9CA90009EADE /* FBSnapshotTestCase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F15AC18D1DBA9CA90009EADE /* FBSnapshotTestCase.framework */; }; F15AC1901DBA9CC60009EADE /* FBSnapshotTestCase.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = F15AC18D1DBA9CA90009EADE /* FBSnapshotTestCase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1048,6 +1049,7 @@ F1510BBC1D5A8146000731AD /* stp_card_jcb_template.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = stp_card_jcb_template.png; sourceTree = ""; }; F1510BBD1D5A8146000731AD /* stp_card_jcb_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_jcb_template@2x.png"; sourceTree = ""; }; F1510BBE1D5A8146000731AD /* stp_card_jcb_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_jcb_template@3x.png"; sourceTree = ""; }; + F15232301EA93E6800D65C67 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; F156753F1DB544D3004468E3 /* STPAddCardViewControllerLocalizationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPAddCardViewControllerLocalizationTests.m; sourceTree = ""; }; F15AC18D1DBA9CA90009EADE /* FBSnapshotTestCase.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FBSnapshotTestCase.framework; path = Carthage/Build/iOS/FBSnapshotTestCase.framework; sourceTree = ""; }; F1852F911D80B6EC00367C86 /* STPStringUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPStringUtils.h; sourceTree = ""; }; @@ -1109,11 +1111,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F1D64B2E1D87686E001CDB7C /* WebKit.framework in Frameworks */, F116E94B1D83404D0026A52A /* AddressBook.framework in Frameworks */, + F15232311EA93E6800D65C67 /* Contacts.framework in Frameworks */, F116E94C1D83405E0026A52A /* Foundation.framework in Frameworks */, 04533E7D1A6877F400C7E52E /* PassKit.framework in Frameworks */, F116E94D1D8340640026A52A /* Security.framework in Frameworks */, + F1D64B2E1D87686E001CDB7C /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1451,6 +1454,7 @@ 11C74B9A164043050071C2CA /* Frameworks */ = { isa = PBXGroup; children = ( + F15232301EA93E6800D65C67 /* Contacts.framework */, C11B14961E8AE316000F760C /* OCMock.framework */, F15AC18D1DBA9CA90009EADE /* FBSnapshotTestCase.framework */, F1D64B2D1D87686E001CDB7C /* WebKit.framework */, diff --git a/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m b/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m index 6629e4a349..2e937ba2ef 100644 --- a/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m +++ b/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m @@ -62,6 +62,20 @@ typedef void (^STPPaymentAuthorizationStatusCallback)(PKPaymentAuthorizationStat }); } +- (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewController *)controller + didSelectShippingContact:(PKContact *)contact + completion:(STPApplePayShippingAddressCompletionBlock)completion { + STPAddress *stpAddress = [[STPAddress alloc] initWithPKContact:contact]; + self.onShippingAddressSelection(stpAddress, ^(STPShippingStatus status, NSArray* shippingMethods, NSArray *summaryItems) { + if (status == STPShippingStatusInvalid) { + completion(PKPaymentAuthorizationStatusInvalidShippingPostalAddress, shippingMethods, summaryItems); + } + else { + completion(PKPaymentAuthorizationStatusSuccess, shippingMethods, summaryItems); + } + }); +} + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" - (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewController *)controller diff --git a/Stripe/PublicHeaders/STPAddress.h b/Stripe/PublicHeaders/STPAddress.h index 9c4015eff2..41a5a88b31 100644 --- a/Stripe/PublicHeaders/STPAddress.h +++ b/Stripe/PublicHeaders/STPAddress.h @@ -6,21 +6,21 @@ // Copyright © 2016 Stripe, Inc. All rights reserved. // -#define FAUXPAS_IGNORED_IN_FILE(...) -FAUXPAS_IGNORED_IN_FILE(APIAvailability) +#define FAUXPAS_IGNORED_IN_METHOD(...) +#define FAUXPAS_IGNORED_ON_LINE(...) + +#import +#import #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" #import #pragma clang diagnostic pop -#define FAUXPAS_IGNORED_IN_METHOD(...) -#define FAUXPAS_IGNORED_ON_LINE(...) - -#import -#import #import "STPAPIResponseDecodable.h" +@class CNContact; + NS_ASSUME_NONNULL_BEGIN /** @@ -100,12 +100,16 @@ typedef NS_ENUM(NSUInteger, STPBillingAddressFields) { - (instancetype)initWithABRecord:(ABRecordRef)record; - (ABRecordRef)ABRecordValue; #pragma clang diagnostic pop -- (PKContact *)PKContactValue NS_AVAILABLE_IOS(9.0); + +- (instancetype)initWithPKContact:(PKContact *)contact NS_AVAILABLE_IOS(9_0); FAUXPAS_IGNORED_ON_LINE(APIAvailability); +- (PKContact *)PKContactValue NS_AVAILABLE_IOS(9_0); FAUXPAS_IGNORED_ON_LINE(APIAvailability); + +- (instancetype)initWithCNContact:(CNContact *)contact NS_AVAILABLE_IOS(9_0); FAUXPAS_IGNORED_ON_LINE(APIAvailability); - (BOOL)containsRequiredFields:(STPBillingAddressFields)requiredFields; - (BOOL)containsRequiredShippingAddressFields:(PKAddressField)requiredFields; -+ (PKAddressField)applePayAddressFieldsFromBillingAddressFields:(STPBillingAddressFields)billingAddressFields; FAUXPAS_IGNORED_ON_LINE(APIAvailability); ++ (PKAddressField)applePayAddressFieldsFromBillingAddressFields:(STPBillingAddressFields)billingAddressFields; @end diff --git a/Stripe/STPAPIClient+ApplePay.m b/Stripe/STPAPIClient+ApplePay.m index 9d5541fd90..e1cd9361b3 100644 --- a/Stripe/STPAPIClient+ApplePay.m +++ b/Stripe/STPAPIClient+ApplePay.m @@ -24,14 +24,7 @@ FAUXPAS_IGNORED_IN_FILE(APIAvailability) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" -+ (NSDictionary *)parametersForPayment:(PKPayment *)payment { - NSCAssert(payment != nil, @"Cannot create a token with a nil payment."); - NSString *paymentString = - [[NSString alloc] initWithData:payment.token.paymentData encoding:NSUTF8StringEncoding]; - NSMutableDictionary *payload = [NSMutableDictionary new]; - payload[@"pk_token"] = paymentString; - - ABRecordRef billingAddress = payment.billingAddress; ++ (NSDictionary *)addressParamsFromABRecord:(ABRecordRef)billingAddress { if (billingAddress) { NSMutableDictionary *params = [NSMutableDictionary dictionary]; @@ -66,10 +59,58 @@ FAUXPAS_IGNORED_IN_FILE(APIAvailability) params[@"address_country"] = country; } CFRelease(dict); - payload[@"card"] = params; } CFRelease(addressValues); } + return params; + } + else { + return nil; + } + +} +#pragma clang diagnostic pop + ++ (NSDictionary *)addressParamsFromPKContact:(PKContact *)billingContact { + if (billingContact) { + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + + NSPersonNameComponents *nameComponents = billingContact.name; + if (nameComponents) { + params[@"name"] = [NSPersonNameComponentsFormatter localizedStringFromPersonNameComponents:nameComponents + style:NSPersonNameComponentsFormatterStyleDefault + options:(NSPersonNameComponentsFormatterOptions)0]; + } + + CNPostalAddress *address = billingContact.postalAddress; + if (address) { + params[@"address_line1"] = address.street; + params[@"address_city"] = address.city; + params[@"address_state"] = address.state; + params[@"address_zip"] = address.postalCode; + params[@"address_country"] = address.ISOCountryCode; + } + + return params; + } + else { + return nil; + } +} + ++ (NSDictionary *)parametersForPayment:(PKPayment *)payment { + NSCAssert(payment != nil, @"Cannot create a token with a nil payment."); + NSString *paymentString = + [[NSString alloc] initWithData:payment.token.paymentData encoding:NSUTF8StringEncoding]; + NSMutableDictionary *payload = [NSMutableDictionary new]; + payload[@"pk_token"] = paymentString; + + if ([PKContact class] + && [payment respondsToSelector:@selector(billingContact)]) { + payload[@"card"] = [self addressParamsFromPKContact:payment.billingContact]; + } + else { + payload[@"card"] = [self addressParamsFromABRecord:payment.billingAddress]; } NSString *paymentInstrumentName = payment.token.paymentInstrumentName; @@ -93,7 +134,6 @@ FAUXPAS_IGNORED_IN_FILE(APIAvailability) return payload; } -#pragma clang diagnostic pop @end diff --git a/Stripe/STPAddress.m b/Stripe/STPAddress.m index 1d2fa0e186..f6fc4442bc 100644 --- a/Stripe/STPAddress.m +++ b/Stripe/STPAddress.m @@ -13,13 +13,18 @@ #import "STPPhoneNumberValidator.h" #import "STPPostalCodeValidator.h" +#import + #define FAUXPAS_IGNORED_IN_FILE(...) FAUXPAS_IGNORED_IN_FILE(APIAvailability) +NSString *stringIfHasContentsElseNil(NSString *string); + @interface STPAddress () @property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; - +@property (nonatomic, readwrite, nullable, copy) NSString *givenName; +@property (nonatomic, readwrite, nullable, copy) NSString *familyName; @end @implementation STPAddress @@ -126,6 +131,61 @@ FAUXPAS_IGNORED_IN_FILE(APIAvailability) #pragma clang diagnostic pop +- (NSString *)sanitizedPhoneStringFromCNPhoneNumber:(CNPhoneNumber *)phoneNumber { + NSString *phone = phoneNumber.stringValue; + if (phone) { + phone = [STPCardValidator sanitizedNumericStringForString:phone]; + } + + return stringIfHasContentsElseNil(phone); +} + +- (instancetype)initWithCNContact:(CNContact *)contact { + self = [super init]; + if (self) { + + _givenName = stringIfHasContentsElseNil(contact.givenName); + _familyName = stringIfHasContentsElseNil(contact.familyName); + _name = stringIfHasContentsElseNil([CNContactFormatter stringFromContact:contact + style:CNContactFormatterStyleFullName]); + _email = stringIfHasContentsElseNil([contact.emailAddresses firstObject].value); + _phone = [self sanitizedPhoneStringFromCNPhoneNumber:contact.phoneNumbers.firstObject.value]; + + + [self setAddressFromCNPostalAddress:contact.postalAddresses.firstObject.value]; + } + return self; +} + +- (instancetype)initWithPKContact:(PKContact *)contact { + self = [super init]; + if (self) { + NSPersonNameComponents *nameComponents = contact.name; + if (nameComponents) { + _givenName = stringIfHasContentsElseNil(nameComponents.givenName); + _familyName = stringIfHasContentsElseNil(nameComponents.familyName); + _name = stringIfHasContentsElseNil([NSPersonNameComponentsFormatter localizedStringFromPersonNameComponents:nameComponents + style:NSPersonNameComponentsFormatterStyleDefault + options:(NSPersonNameComponentsFormatterOptions)0]); + } + _email = stringIfHasContentsElseNil(contact.emailAddress); + _phone = [self sanitizedPhoneStringFromCNPhoneNumber:contact.phoneNumber]; + [self setAddressFromCNPostalAddress:contact.postalAddress]; + + } + return self; +} + +- (void)setAddressFromCNPostalAddress:(CNPostalAddress *)address { + if (address) { + _line1 = stringIfHasContentsElseNil(address.street); + _city = stringIfHasContentsElseNil(address.city); + _state = stringIfHasContentsElseNil(address.state); + _postalCode = stringIfHasContentsElseNil(address.postalCode); + _country = stringIfHasContentsElseNil(address.ISOCountryCode.uppercaseString); + } +} + - (PKContact *)PKContactValue { PKContact *contact = [PKContact new]; NSPersonNameComponents *name = [NSPersonNameComponents new]; @@ -145,19 +205,29 @@ FAUXPAS_IGNORED_IN_FILE(APIAvailability) } - (NSString *)firstName { - NSArray*components = [self.name componentsSeparatedByString:@" "]; - return [components firstObject]; + if (self.givenName) { + return self.givenName; + } + else { + NSArray*components = [self.name componentsSeparatedByString:@" "]; + return [components firstObject]; + } } - (NSString *)lastName { - NSArray*components = [self.name componentsSeparatedByString:@" "]; - NSString *firstName = [components firstObject]; - NSString *lastName = [self.name stringByReplacingOccurrencesOfString:firstName withString:@""]; - lastName = [lastName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - if ([lastName length] == 0) { - lastName = nil; + if (self.familyName) { + return self.familyName; + } + else { + NSArray*components = [self.name componentsSeparatedByString:@" "]; + NSString *firstName = [components firstObject]; + NSString *lastName = [self.name stringByReplacingOccurrencesOfString:firstName withString:@""]; + lastName = [lastName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if ([lastName length] == 0) { + lastName = nil; + } + return lastName; } - return lastName; } - (NSString *)street { @@ -247,3 +317,12 @@ FAUXPAS_IGNORED_IN_FILE(APIAvailability) @end +NSString *stringIfHasContentsElseNil(NSString *string) { + if (string.length > 0) { + return string; + } + else { + return nil; + } +} + diff --git a/Tests/Tests/STPAddressTests.m b/Tests/Tests/STPAddressTests.m index 5953a6c627..3dfaebbb2b 100644 --- a/Tests/Tests/STPAddressTests.m +++ b/Tests/Tests/STPAddressTests.m @@ -8,6 +8,8 @@ #import #import +#import +#import #import "STPAddress.h" @interface STPAddressTests : XCTestCase @@ -16,7 +18,134 @@ @implementation STPAddressTests -- (void)testInit { +- (void)testInitWithPKContact_complete { + PKContact *contact = [PKContact new]; + { + NSPersonNameComponents *name = [NSPersonNameComponents new]; + name.givenName = @"John"; + name.familyName = @"Doe"; + contact.name = name; + + contact.emailAddress = @"foo@example.com"; + contact.phoneNumber = [CNPhoneNumber phoneNumberWithStringValue:@"888-555-1212"]; + + CNMutablePostalAddress *address = [CNMutablePostalAddress new]; + address.street = @"55 John St"; + address.city = @"New York"; + address.state = @"NY"; + address.postalCode = @"10002"; + address.ISOCountryCode = @"US"; + address.country = @"United States"; + contact.postalAddress = address.copy; + } + + STPAddress *address = [[STPAddress alloc] initWithPKContact:contact]; + XCTAssertEqualObjects(@"John Doe", address.name); + XCTAssertEqualObjects(@"8885551212", address.phone); + XCTAssertEqualObjects(@"foo@example.com", address.email); + XCTAssertEqualObjects(@"55 John St", address.line1); + XCTAssertEqualObjects(@"New York", address.city); + XCTAssertEqualObjects(@"NY", address.state); + XCTAssertEqualObjects(@"10002", address.postalCode); + XCTAssertEqualObjects(@"US", address.country); +} + +- (void)testInitWithPKContact_partial { + PKContact *contact = [PKContact new]; + { + NSPersonNameComponents *name = [NSPersonNameComponents new]; + name.givenName = @"John"; + contact.name = name; + + CNMutablePostalAddress *address = [CNMutablePostalAddress new]; + address.state = @"VA"; + contact.postalAddress = address.copy; + } + + STPAddress *address = [[STPAddress alloc] initWithPKContact:contact]; + XCTAssertEqualObjects(@"John", address.name); + XCTAssertNil(address.phone); + XCTAssertNil(address.email); + XCTAssertNil(address.line1); + XCTAssertNil(address.city); + XCTAssertEqualObjects(@"VA", address.state); + XCTAssertNil(address.postalCode); + XCTAssertNil(address.country); +} + +- (void)testInitWithCNContact_complete { + CNMutableContact *contact = [CNMutableContact new]; + { + contact.givenName = @"John"; + contact.familyName = @"Doe"; + + contact.emailAddresses = @[ + [CNLabeledValue labeledValueWithLabel:CNLabelHome + value:@"foo@example.com"], + [CNLabeledValue labeledValueWithLabel:CNLabelWork + value:@"bar@example.com"], + + + ]; + + contact.phoneNumbers = @[ + [CNLabeledValue labeledValueWithLabel:CNLabelHome + value:[CNPhoneNumber phoneNumberWithStringValue:@"888-555-1212"]], + [CNLabeledValue labeledValueWithLabel:CNLabelWork + value:[CNPhoneNumber phoneNumberWithStringValue:@"555-555-5555"]], + + + ]; + + CNMutablePostalAddress *address = [CNMutablePostalAddress new]; + address.street = @"55 John St"; + address.city = @"New York"; + address.state = @"NY"; + address.postalCode = @"10002"; + address.ISOCountryCode = @"US"; + address.country = @"United States"; + contact.postalAddresses = @[ + [CNLabeledValue labeledValueWithLabel:CNLabelHome + value:address], + ]; + } + + STPAddress *address = [[STPAddress alloc] initWithCNContact:contact]; + XCTAssertEqualObjects(@"John Doe", address.name); + XCTAssertEqualObjects(@"8885551212", address.phone); + XCTAssertEqualObjects(@"foo@example.com", address.email); + XCTAssertEqualObjects(@"55 John St", address.line1); + XCTAssertEqualObjects(@"New York", address.city); + XCTAssertEqualObjects(@"NY", address.state); + XCTAssertEqualObjects(@"10002", address.postalCode); + XCTAssertEqualObjects(@"US", address.country); +} + +- (void)testInitWithCNContact_partial { + CNMutableContact *contact = [CNMutableContact new]; + { + contact.givenName = @"John"; + + CNMutablePostalAddress *address = [CNMutablePostalAddress new]; + address.state = @"VA"; + contact.postalAddresses = @[ + [CNLabeledValue labeledValueWithLabel:CNLabelHome + value:address], + ]; + } + + STPAddress *address = [[STPAddress alloc] initWithCNContact:contact]; + XCTAssertEqualObjects(@"John", address.name); + XCTAssertNil(address.phone); + XCTAssertNil(address.email); + XCTAssertNil(address.line1); + XCTAssertNil(address.city); + XCTAssertEqualObjects(@"VA", address.state); + XCTAssertNil(address.postalCode); + XCTAssertNil(address.country); +} + +- (void)testInitWithABRecord_complete { ABRecordRef record = ABPersonCreate(); ABRecordSetValue(record, kABPersonFirstNameProperty, CFSTR("John"), nil); ABRecordSetValue(record, kABPersonLastNameProperty, CFSTR("Doe"), nil); @@ -50,7 +179,7 @@ XCTAssertEqualObjects(@"US", address.country); } -- (void)testInit_partial { +- (void)testInitWithABRecord_partial { ABRecordRef record = ABPersonCreate(); ABRecordSetValue(record, kABPersonFirstNameProperty, CFSTR("John"), nil); ABMutableMultiValueRef addressRef = ABMultiValueCreateMutable(kABMultiDictionaryPropertyType); diff --git a/Tests/Tests/STPTokenTest.m b/Tests/Tests/STPTokenTest.m index 0aafb1b365..6097513bae 100644 --- a/Tests/Tests/STPTokenTest.m +++ b/Tests/Tests/STPTokenTest.m @@ -26,11 +26,11 @@ @"address_city": @"New York", @"address_state": @"NY", @"address_zip": @"12345", - @"address_country": @"USA", + @"address_country": @"US", @"last4": @"1234", @"brand": @"Visa", @"fingerprint": @"Fingolfin", - @"country": @"Japan", + @"country": @"JP", }; NSDictionary *tokenDict = @{ @"id": @"id_for_token", @"object": @"token", @"livemode": @NO, @"created": @1353025450.0, @"used": @NO, @"card": cardDict };