Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ApplePayContext handles shipping #1561

Merged
merged 4 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would appreciate scrutiny on this API change!

// 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
Expand Down
17 changes: 15 additions & 2 deletions Stripe/PublicHeaders/STPApplePayContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion Stripe/PublicHeaders/STPBlocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

27 changes: 26 additions & 1 deletion Stripe/STPApplePayContext.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This produces a name like "Dr. Jonathan Maple Appleseed Esq." vs. ...StyleDefault which produces a name like "Jonathan Appleseed".

STPPaymentIntentShippingDetailsParams *shippingParams = [[STPPaymentIntentShippingDetailsParams alloc] initWithAddress:addressParams name:[formatter stringFromPersonNameComponents:name]];
shippingParams.phone = payment.shippingContact.phoneNumber.stringValue;

return shippingParams;
}

#pragma mark - PKPaymentAuthorizationViewControllerDelegate

Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down
21 changes: 12 additions & 9 deletions Tests/Tests/STPApplePayContextFunctionalTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

@interface STPTestApplePayContextDelegate: NSObject <STPApplePayContextDelegate>
@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

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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];
}];
};
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 43 additions & 2 deletions Tests/Tests/STPApplePayContextTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
#import <XCTest/XCTest.h>

#import "STPApplePayContext.h"
#import "STPFixtures.h"
#import "Stripe.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"

@interface STPApplePayContext (Private) <PKPaymentAuthorizationViewControllerDelegate>
- (STPPaymentIntentShippingDetailsParams *)_shippingDetailsFromPKPayment:(PKPayment *)payment;
@end

@interface STPApplePayTestDelegateiOS11 : NSObject <STPApplePayContextDelegate>
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions Tests/Tests/STPFixtures.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down