From 3a6b986dccf9dae8a818c4c807c246464251ab27 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 28 Apr 2020 10:06:21 -0700 Subject: [PATCH 1/4] ApplePayContext handles shipping --- Stripe/PublicHeaders/STPApplePayContext.h | 16 ++++++- Stripe/STPApplePayContext.m | 27 ++++++++++- .../Tests/STPApplePayContextFunctionalTest.m | 21 +++++---- Tests/Tests/STPApplePayContextTest.m | 45 ++++++++++++++++++- Tests/Tests/STPFixtures.m | 8 ++++ 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/Stripe/PublicHeaders/STPApplePayContext.h b/Stripe/PublicHeaders/STPApplePayContext.h index 0bb65f4195a..6e87bf6134c 100644 --- a/Stripe/PublicHeaders/STPApplePayContext.h +++ b/Stripe/PublicHeaders/STPApplePayContext.h @@ -24,12 +24,17 @@ NS_ASSUME_NONNULL_BEGIN /** Called after the customer has authorized Apple Pay. Implement this method to call the completion block with the client secret of a PaymentIntent representing the payment. - @param paymentMethod The PaymentMethod that represents the customer's Apple Pay payment method. + @param paymentMethod The PaymentMethod that represents the customer's Apple Pay payment method. If you create the PaymentIntent with confirmation_method=manual, pass `paymentMethod.stripeId` as the payment_method and confirm=true. Otherwise, you can ignore this parameter. - @param completion Call this with the PaymentIntent's client secret, or the error that occurred creating the PaymentIntent. + + @param paymentInformation The underlying PKPayment created by Apple Pay. + If you create the PaymentIntent with confirmation_method=manual, you can collect shipping information using its `shippingContact` and `shippingMethod` properties. Otherwise, you can ignore this parameter. + + @param completion Call this with the PaymentIntent's client secret, or the error that occurred creating the PaymentIntent. */ - (void)applePayContext:(STPApplePayContext *)context didCreatePaymentMethod:(STPPaymentMethod *)paymentMethod + paymentInformation:(PKPayment *)paymentInformation completion:(STPIntentClientSecretCompletionBlock)completion; /** @@ -61,6 +66,8 @@ didSelectShippingMethod:(PKShippingMethod *)shippingMethod /** Called when the user has selected a new shipping address. You should inspect the address and must invoke the completion block with an updated array of PKPaymentSummaryItem objects. + + @note This does not contain full contact information - you can only receive that after the user authorizes payment, in the paymentInformation passed to `applePayContext:didCreatePaymentMethod:paymentInformation:completion:` */ - (void)applePayContext:(STPApplePayContext *)context didSelectShippingContact:(PKContact *)contact @@ -112,6 +119,11 @@ didSelectShippingMethod:(PKShippingMethod *)shippingMethod */ - (instancetype)init NS_UNAVAILABLE; +/** + Use initWithPaymentRequest:delegate: instead. + */ ++ (instancetype)new NS_UNAVAILABLE; + /** Presents the Apple Pay sheet, starting the payment process. diff --git a/Stripe/STPApplePayContext.m b/Stripe/STPApplePayContext.m index 4d1991843f5..0bfcec1e2b2 100644 --- a/Stripe/STPApplePayContext.m +++ b/Stripe/STPApplePayContext.m @@ -14,6 +14,8 @@ #import "STPAPIClient+ApplePay.h" #import "STPPaymentMethod.h" #import "STPPaymentIntentParams.h" +#import "STPPaymentIntentShippingDetailsParams.h" +#import "STPPaymentIntentShippingDetailsAddressParams.h" #import "STPPaymentIntent+Private.h" #import "STPPaymentHandler.h" #import "NSError+Stripe.h" @@ -124,6 +126,28 @@ - (void)_end { self.viewController = nil; self.delegate = nil; } + +- (nullable STPPaymentIntentShippingDetailsParams *)_shippingDetailsFromPKPayment:(PKPayment *)payment { + CNPostalAddress *address = payment.shippingContact.postalAddress; + NSPersonNameComponents *name = payment.shippingContact.name; + if (address.street == nil || name == nil) { + // The shipping address street and name are required parameters for a valid STPPaymentIntentShippingDetailsParams + return nil; + } + + STPPaymentIntentShippingDetailsAddressParams *addressParams = [[STPPaymentIntentShippingDetailsAddressParams alloc] initWithLine1:payment.shippingContact.postalAddress.street]; + addressParams.city = address.city; + addressParams.state = address.state; + addressParams.country = address.ISOCountryCode; + addressParams.postalCode = address.postalCode; + + NSPersonNameComponentsFormatter *formatter = [NSPersonNameComponentsFormatter new]; + formatter.style = NSPersonNameComponentsFormatterStyleLong; + STPPaymentIntentShippingDetailsParams *shippingParams = [[STPPaymentIntentShippingDetailsParams alloc] initWithAddress:addressParams name:[formatter stringFromPersonNameComponents:name]]; + shippingParams.phone = payment.shippingContact.phoneNumber.stringValue; + + return shippingParams; +} #pragma mark - PKPaymentAuthorizationViewControllerDelegate @@ -258,7 +282,7 @@ - (void)_completePaymentWithPayment:(PKPayment *)payment completion:(nonnull voi } // 2. Fetch PaymentIntent client secret from delegate - [self.delegate applePayContext:self didCreatePaymentMethod:paymentMethod completion:^(NSString * _Nullable paymentIntentClientSecret, NSError * _Nullable paymentIntentCreationError) { + [self.delegate applePayContext:self didCreatePaymentMethod:paymentMethod paymentInformation:payment completion:^(NSString * _Nullable paymentIntentClientSecret, NSError * _Nullable paymentIntentCreationError) { if (paymentIntentCreationError || !self.viewController) { handleFinalState(STPPaymentStateError, paymentIntentCreationError); return; @@ -275,6 +299,7 @@ - (void)_completePaymentWithPayment:(PKPayment *)payment completion:(nonnull voi STPPaymentIntentParams *paymentIntentParams = [[STPPaymentIntentParams alloc] initWithClientSecret:paymentIntentClientSecret]; paymentIntentParams.paymentMethodId = paymentMethod.stripeId; paymentIntentParams.useStripeSDK = @(YES); + paymentIntentParams.shipping = [self _shippingDetailsFromPKPayment:payment]; self.paymentState = STPPaymentStatePending; diff --git a/Tests/Tests/STPApplePayContextFunctionalTest.m b/Tests/Tests/STPApplePayContextFunctionalTest.m index 68e080bc86a..4153b459ce1 100644 --- a/Tests/Tests/STPApplePayContextFunctionalTest.m +++ b/Tests/Tests/STPApplePayContextFunctionalTest.m @@ -18,7 +18,7 @@ @interface STPTestApplePayContextDelegate: NSObject @property (nonatomic) void (^didCompleteDelegateMethod)(STPPaymentStatus status, NSError *error); -@property (nonatomic) void (^didCreatePaymentMethodDelegateMethod)(STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion); +@property (nonatomic) void (^didCreatePaymentMethodDelegateMethod)(STPPaymentMethod *paymentMethod, PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion); @end @@ -28,8 +28,8 @@ - (void)applePayContext:(__unused STPApplePayContext *)context didCompleteWithSt self.didCompleteDelegateMethod(status, error); } -- (void)applePayContext:(__unused STPApplePayContext *)context didCreatePaymentMethod:(STPPaymentMethod *)paymentMethod completion:(nonnull STPIntentClientSecretCompletionBlock)completion { - self.didCreatePaymentMethodDelegateMethod(paymentMethod, completion); +- (void)applePayContext:(__unused STPApplePayContext *)context didCreatePaymentMethod:(STPPaymentMethod *)paymentMethod paymentInformation:(PKPayment *)paymentInformation completion:(nonnull STPIntentClientSecretCompletionBlock)completion { + self.didCreatePaymentMethodDelegateMethod(paymentMethod, paymentInformation, completion); } @end @@ -70,7 +70,8 @@ - (void)testCompletesManualConfirmationPaymentIntent { __block NSString *clientSecret; // A manual confirmation PI confirmed server-side... STPTestApplePayContextDelegate *delegate = self.delegate; - delegate.didCreatePaymentMethodDelegateMethod = ^(STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion) { + delegate.didCreatePaymentMethodDelegateMethod = ^(STPPaymentMethod *paymentMethod, PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion) { + XCTAssertNotNil(paymentInformation); [[STPTestingAPIClient sharedClient] createPaymentIntentWithParams:@{@"confirmation_method": @"manual", @"payment_method": paymentMethod.stripeId, @"confirm": @YES} completion:^(NSString * _Nullable _clientSecret, NSError * __unused error) { XCTAssertNotNil(_clientSecret); clientSecret = _clientSecret; @@ -104,7 +105,7 @@ - (void)testCompletesAutomaticConfirmationPaymentIntent { __block NSString *clientSecret; // An automatic confirmation PI with the PaymentMethod attached... STPTestApplePayContextDelegate *delegate = self.delegate; - delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion) { + delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, __unused PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion) { [[STPTestingAPIClient sharedClient] createPaymentIntentWithParams:nil completion:^(NSString * _Nullable _clientSecret, NSError * __unused error) { XCTAssertNotNil(_clientSecret); clientSecret = _clientSecret; @@ -125,6 +126,8 @@ - (void)testCompletesAutomaticConfirmationPaymentIntent { [self.apiClient retrievePaymentIntentWithClientSecret:clientSecret completion:^(STPPaymentIntent * _Nullable paymentIntent, NSError *paymentIntentRetrieveError) { XCTAssertNil(paymentIntentRetrieveError); XCTAssert(paymentIntent.status == STPPaymentIntentStatusSucceeded); + XCTAssertEqualObjects(paymentIntent.shipping.name, @"Jane Doe"); + XCTAssertEqualObjects(paymentIntent.shipping.address.line1, @"510 Townsend St"); [didCallCompletion fulfill]; }]; }; @@ -136,7 +139,7 @@ - (void)testCompletesAutomaticConfirmationPaymentIntentManualCapture { __block NSString *clientSecret; // An automatic confirmation PI with the PaymentMethod attached... STPTestApplePayContextDelegate *delegate = self.delegate; - delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion) { + delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, __unused PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion) { [[STPTestingAPIClient sharedClient] createPaymentIntentWithParams:@{@"capture_method": @"manual"} completion:^(NSString * _Nullable _clientSecret, NSError * __unused error) { XCTAssertNotNil(_clientSecret); clientSecret = _clientSecret; @@ -168,7 +171,7 @@ - (void)testBadPaymentIntentClientSecretErrors { __block NSString *clientSecret; // An invalid PaymentIntent client secret... STPTestApplePayContextDelegate *delegate = self.delegate; - delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion) { + delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, __unused PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion) { dispatch_async(dispatch_get_main_queue(), ^{ clientSecret = @"pi_bad_secret_1234"; completion(clientSecret, nil); @@ -194,7 +197,7 @@ - (void)testBadPaymentIntentClientSecretErrors { - (void)testCancelBeforePaymentIntentConfirmsCancels { // Cancelling Apple Pay *before* the context attempts to confirms the PI... STPTestApplePayContextDelegate *delegate = self.delegate; - delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion) { + delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, __unused PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion) { [self.context paymentAuthorizationViewControllerDidFinish:self.context.viewController]; // Simulate cancel before passing PI to the context // ...should never retrieve the PI (b/c it is cancelled before) completion(@"A 'client secret' that triggers an exception if fetched", nil); @@ -225,7 +228,7 @@ - (void)testCancelAfterPaymentIntentConfirmsStillSucceeds { __block NSString *clientSecret; STPTestApplePayContextDelegate *delegate = self.delegate; - delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, STPIntentClientSecretCompletionBlock completion) { + delegate.didCreatePaymentMethodDelegateMethod = ^(__unused STPPaymentMethod *paymentMethod, __unused PKPayment *paymentInformation, STPIntentClientSecretCompletionBlock completion) { [[STPTestingAPIClient sharedClient] createPaymentIntentWithParams:nil completion:^(NSString * _Nullable _clientSecret, NSError * __unused error) { XCTAssertNotNil(_clientSecret); clientSecret = _clientSecret; diff --git a/Tests/Tests/STPApplePayContextTest.m b/Tests/Tests/STPApplePayContextTest.m index 2f84aba255e..52728a1aa82 100644 --- a/Tests/Tests/STPApplePayContextTest.m +++ b/Tests/Tests/STPApplePayContextTest.m @@ -9,12 +9,14 @@ #import #import "STPApplePayContext.h" +#import "STPFixtures.h" #import "Stripe.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-parameter" @interface STPApplePayContext (Private) +- (STPPaymentIntentShippingDetailsParams *)_shippingDetailsFromPKPayment:(PKPayment *)payment; @end @interface STPApplePayTestDelegateiOS11 : NSObject @@ -33,7 +35,10 @@ - (void)applePayContext:(__unused STPApplePayContext *)context didSelectShipping } - (void)applePayContext:(__unused STPApplePayContext *)context didCompleteWithStatus:(__unused STPPaymentStatus)status error:(__unused NSError *)error {} -- (void)applePayContext:(__unused STPApplePayContext *)context didCreatePaymentMethod:(__unused NSString *)paymentMethodID completion:(__unused STPIntentClientSecretCompletionBlock)completion {} + +- (void)applePayContext:(nonnull STPApplePayContext *)context didCreatePaymentMethod:(nonnull STPPaymentMethod *)paymentMethod paymentInformation:(nonnull PKPayment *)paymentInformation completion:(nonnull STPIntentClientSecretCompletionBlock)completion { + +} @end @@ -53,7 +58,10 @@ - (void)applePayContext:(__unused STPApplePayContext *)context didSelectShipping } - (void)applePayContext:(__unused STPApplePayContext *)context didCompleteWithStatus:(__unused STPPaymentStatus)status error:(__unused NSError *)error {} -- (void)applePayContext:(__unused STPApplePayContext *)context didCreatePaymentMethod:(__unused NSString *)paymentMethodID completion:(__unused STPIntentClientSecretCompletionBlock)completion {} + +- (void)applePayContext:(nonnull STPApplePayContext *)context didCreatePaymentMethod:(nonnull STPPaymentMethod *)paymentMethod paymentInformation:(nonnull PKPayment *)paymentInformation completion:(nonnull STPIntentClientSecretCompletionBlock)completion { +} + @end @@ -129,6 +137,39 @@ - (void)testiOS10ApplePayDelegateMethodsForwarded { [self waitForExpectationsWithTimeout:2 handler:nil]; } +- (void)testConvertsShippingDetails { + STPApplePayTestDelegateiOS10 *delegate = [STPApplePayTestDelegateiOS10 new]; + PKPaymentRequest *request = [Stripe paymentRequestWithMerchantIdentifier:@"foo" country:@"US" currency:@"USD"]; + request.paymentSummaryItems = @[[PKPaymentSummaryItem summaryItemWithLabel:@"bar" amount:[NSDecimalNumber decimalNumberWithString:@"1.00"]]]; + STPApplePayContext *context = [[STPApplePayContext alloc] initWithPaymentRequest:request delegate:delegate]; + + PKPayment *payment = [STPFixtures simulatorApplePayPayment]; + PKContact *shipping = [PKContact new]; + shipping.name = [[NSPersonNameComponentsFormatter new] personNameComponentsFromString:@"Jane Doe"]; + shipping.phoneNumber = [[CNPhoneNumber alloc] initWithStringValue:@"555-555-5555"]; + CNMutablePostalAddress *address = [CNMutablePostalAddress new]; + address.street = @"510 Townsend St"; + address.city = @"San Francisco"; + address.state = @"CA"; + address.ISOCountryCode = @"US"; + address.postalCode = @"94105"; + shipping.postalAddress = address; + [payment performSelector:@selector(setShippingContact:) withObject:shipping]; + + STPPaymentIntentShippingDetailsParams *shippingParams = [context _shippingDetailsFromPKPayment:payment]; + XCTAssertNotNil(shippingParams); + XCTAssertEqualObjects(shippingParams.name, @"Jane Doe"); + XCTAssertNil(shippingParams.carrier); + XCTAssertEqualObjects(shippingParams.phone, @"555-555-5555"); + XCTAssertNil(shippingParams.trackingNumber); + + XCTAssertEqualObjects(shippingParams.address.line1, @"510 Townsend St"); + XCTAssertNil(shippingParams.address.line2); + XCTAssertEqualObjects(shippingParams.address.city, @"San Francisco"); + XCTAssertEqualObjects(shippingParams.address.state, @"CA"); + XCTAssertEqualObjects(shippingParams.address.country, @"US"); + XCTAssertEqualObjects(shippingParams.address.postalCode, @"94105"); +} #pragma clang diagnostic pop diff --git a/Tests/Tests/STPFixtures.m b/Tests/Tests/STPFixtures.m index 4fdfbdf4699..6c46e0430b1 100644 --- a/Tests/Tests/STPFixtures.m +++ b/Tests/Tests/STPFixtures.m @@ -264,6 +264,14 @@ + (PKPayment *)simulatorApplePayPayment { [paymentToken performSelector:@selector(setPaymentMethod:) withObject:paymentMethod]; [payment performSelector:@selector(setToken:) withObject:paymentToken]; + + // Add shipping + PKContact *shipping = [PKContact new]; + shipping.name = [[NSPersonNameComponentsFormatter new] personNameComponentsFromString:@"Jane Doe"]; + CNMutablePostalAddress *address = [CNMutablePostalAddress new]; + address.street = @"510 Townsend St"; + shipping.postalAddress = address; + [payment performSelector:@selector(setShippingContact:) withObject:shipping]; #pragma clang diagnostic pop return payment; } From 2c02214649074375ded1fa8d92375a416d642dc8 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Wed, 29 Apr 2020 13:21:25 -0700 Subject: [PATCH 2/4] Fix ApplePayExampleViewController --- .../Non-Card Payment Examples/ApplePayExampleViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/Non-Card Payment Examples/ApplePayExampleViewController.m b/Example/Non-Card Payment Examples/ApplePayExampleViewController.m index 194062464ab..6c569067ed5 100644 --- a/Example/Non-Card Payment Examples/ApplePayExampleViewController.m +++ b/Example/Non-Card Payment Examples/ApplePayExampleViewController.m @@ -90,7 +90,7 @@ - (void)pay { #pragma mark - STPApplePayContextDelegate -- (void)applePayContext:(STPApplePayContext *)context didCreatePaymentMethod:(__unused STPPaymentMethod *)paymentMethod completion:(STPIntentClientSecretCompletionBlock)completion { +- (void)applePayContext:(STPApplePayContext *)context didCreatePaymentMethod:(__unused STPPaymentMethod *)paymentMethod paymentInformation:(__unused PKPayment *)paymentInformation completion:(STPIntentClientSecretCompletionBlock)completion { // Create the Stripe PaymentIntent representing the payment on our backend [[MyAPIClient sharedClient] createPaymentIntentWithCompletion:^(MyAPIClientResult status, NSString *clientSecret, NSError *error) { // Call the completion block with the PaymentIntent's client secret From b2fe686afbb91b47f42c7d22acf7dde64b00cdab Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Thu, 30 Apr 2020 12:15:37 -0700 Subject: [PATCH 3/4] Improve docstrings --- Stripe/PublicHeaders/STPApplePayContext.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Stripe/PublicHeaders/STPApplePayContext.h b/Stripe/PublicHeaders/STPApplePayContext.h index 6e87bf6134c..64d18a24005 100644 --- a/Stripe/PublicHeaders/STPApplePayContext.h +++ b/Stripe/PublicHeaders/STPApplePayContext.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN If you create the PaymentIntent with confirmation_method=manual, pass `paymentMethod.stripeId` as the payment_method and confirm=true. Otherwise, you can ignore this parameter. @param paymentInformation The underlying PKPayment created by Apple Pay. - If you create the PaymentIntent with confirmation_method=manual, you can collect shipping information using its `shippingContact` and `shippingMethod` properties. Otherwise, you can ignore this parameter. + If you create the PaymentIntent with confirmation_method=manual, you can collect shipping information using its `shippingContact` and `shippingMethod` properties. @param completion Call this with the PaymentIntent's client secret, or the error that occurred creating the PaymentIntent. */ @@ -67,7 +67,8 @@ didSelectShippingMethod:(PKShippingMethod *)shippingMethod Called when the user has selected a new shipping address. You should inspect the address and must invoke the completion block with an updated array of PKPaymentSummaryItem objects. - @note This does not contain full contact information - you can only receive that after the user authorizes payment, in the paymentInformation passed to `applePayContext:didCreatePaymentMethod:paymentInformation:completion:` + @note To maintain privacy, the shipping information is anonymized. For example, in the United States it only includes the city, state, and zip code. This provides enough information to calculate shipping costs, without revealing sensitive information until the user actually approves the purchase. + Receive full shipping information in the paymentInformation passed to `applePayContext:didCreatePaymentMethod:paymentInformation:completion:` */ - (void)applePayContext:(STPApplePayContext *)context didSelectShippingContact:(PKContact *)contact From 6fdf57c5fbac638ef4e5cc1cfe875639b14d668b Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Thu, 30 Apr 2020 14:11:26 -0700 Subject: [PATCH 4/4] Improve docstrings --- Stripe/PublicHeaders/STPBlocks.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stripe/PublicHeaders/STPBlocks.h b/Stripe/PublicHeaders/STPBlocks.h index 2935d26316e..d9be6f7231b 100644 --- a/Stripe/PublicHeaders/STPBlocks.h +++ b/Stripe/PublicHeaders/STPBlocks.h @@ -249,7 +249,7 @@ typedef void (^STPPaymentStatusBlock)(STPPaymentStatus status, NSError * __nulla A block to be run with the client secret of a PaymentIntent or SetupIntent. @param clientSecret The client secret of the PaymentIntent or SetupIntent. See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-client_secret - @param error The error creating the Intent, or nil if none occurred. + @param error The error that occurred when creating the Intent, or nil if none occurred. */ typedef void (^STPIntentClientSecretCompletionBlock)(NSString * __nullable clientSecret, NSError * __nullable error);