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 diff --git a/Stripe/PublicHeaders/STPApplePayContext.h b/Stripe/PublicHeaders/STPApplePayContext.h index 0bb65f4195a..64d18a24005 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. + + @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,9 @@ 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 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 @@ -112,6 +120,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/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); 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; }