From d38bc13cb49a238a68662e4f8e17fc4a8bbb3e27 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 11:27:42 -0700 Subject: [PATCH 01/10] Add defaultPaymentMethod docstring --- Stripe/PublicHeaders/STPPaymentContext.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Stripe/PublicHeaders/STPPaymentContext.h b/Stripe/PublicHeaders/STPPaymentContext.h index 6cdf1ccaafc..2406eac38e9 100644 --- a/Stripe/PublicHeaders/STPPaymentContext.h +++ b/Stripe/PublicHeaders/STPPaymentContext.h @@ -140,6 +140,8 @@ NS_ASSUME_NONNULL_BEGIN The Stripe ID of a payment method to display as the default pre-selected option. Customer doesn't have a default payment method property, but you can store one (in its metadata, for example) and set this property accordingly. + + @note Set this property immediately after initializing STPPaymentContext, or call `retryLoading` afterwards. */ @property (nonatomic, copy, nullable) NSString *defaultPaymentMethod; From a439c25c0aac6e2baceff394a5eee463be07bc82 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 12:24:43 -0700 Subject: [PATCH 02/10] Improve STPPaymentMethodCardParams docstring --- Stripe/PublicHeaders/STPPaymentMethodCardParams.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stripe/PublicHeaders/STPPaymentMethodCardParams.h b/Stripe/PublicHeaders/STPPaymentMethodCardParams.h index c9666c7bd72..b395441659c 100644 --- a/Stripe/PublicHeaders/STPPaymentMethodCardParams.h +++ b/Stripe/PublicHeaders/STPPaymentMethodCardParams.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) NSString *number; /** - Two-digit number representing the card's expiration month. + Number representing the card's expiration month. Ex. @1 */ @property (nonatomic, nullable) NSNumber *expMonth; From 7b5be2b02bde50eea0295e3b159ae1edf0f20c4d Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 12:23:22 -0700 Subject: [PATCH 03/10] Add authenticationWillPresent: to STPAuthenticationContext to support Apple Pay --- Stripe/Payments/STPPaymentHandler.m | 74 +++++++++++++------ .../PublicHeaders/STPAuthenticationContext.h | 12 +++ Stripe/PublicHeaders/STPPaymentHandler.h | 7 +- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/Stripe/Payments/STPPaymentHandler.m b/Stripe/Payments/STPPaymentHandler.m index e2b35a892a8..1c64567e9ab 100644 --- a/Stripe/Payments/STPPaymentHandler.m +++ b/Stripe/Payments/STPPaymentHandler.m @@ -404,14 +404,6 @@ - (BOOL)_handlePaymentIntentStatusForAction:(STPPaymentHandlerPaymentIntentActio - (void)_handleAuthenticationForCurrentAction { STPIntentAction *authenticationAction = _currentAction.nextAction; - // Checking for authenticationPresentingViewController instead of just authenticationContext == nil - // also allows us to catch contexts that are not behaving correctly (i.e. returning nil vc when they shouldn't) - UIViewController *presentingViewController = [_currentAction.authenticationContext authenticationPresentingViewController]; - if (presentingViewController == nil || presentingViewController.view.window == nil) { - [_currentAction completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerRequiresAuthenticationContextErrorCode userInfo:nil]]; - return; - } - switch (authenticationAction.type) { case STPIntentActionTypeUnknown: @@ -478,18 +470,29 @@ - (void)_handleAuthenticationForCurrentAction { [self _retrieveAndCheckIntentForCurrentAction]; return; } - STDSChallengeParameters *challengeParameters = [[STDSChallengeParameters alloc] initWithAuthenticationResponse:aRes]; - @try { - [transaction doChallengeWithViewController:[self->_currentAction.authenticationContext authenticationPresentingViewController] - challengeParameters:challengeParameters - challengeStatusReceiver:self - timeout:self->_currentAction.threeDSCustomizationSettings.authenticationTimeout*60]; - - } @catch (NSException *exception) { - [self->_currentAction completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerStripe3DS2ErrorCode userInfo:@{@"exception": exception}]]; + + if (![self _canPresentWithAuthenticationContext:self->_currentAction.authenticationContext]) { + [self->_currentAction completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerRequiresAuthenticationContextErrorCode userInfo:nil]]; + return; } + STPVoidBlock doChallenge = ^{ + @try { + [transaction doChallengeWithViewController:[self->_currentAction.authenticationContext authenticationPresentingViewController] + challengeParameters:challengeParameters + challengeStatusReceiver:self + timeout:self->_currentAction.threeDSCustomizationSettings.authenticationTimeout*60]; + + } @catch (NSException *exception) { + [self->_currentAction completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerStripe3DS2ErrorCode userInfo:@{@"exception": exception}]]; + } + }; + if ([self->_currentAction.authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]) { + [self->_currentAction.authenticationContext authenticationWillPresent:doChallenge]; + } else { + doChallenge(); + } } }]; } @@ -561,16 +564,23 @@ - (void)_handleRedirectToURL:(NSURL *)url withReturnURL:(nullable NSURL *)return [[STPAnalyticsClient sharedClient] logURLRedirectNextActionWithConfiguration:_currentAction.apiClient.configuration intentID:_currentAction.intentStripeID]; void (^presentSFViewControllerBlock)(void) = ^{ - SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:url]; - safariViewController.delegate = self; UIViewController *presentingViewController = [self->_currentAction.authenticationContext authenticationPresentingViewController]; - if (presentingViewController == nil || presentingViewController.view.window == nil) { + if (![self _canPresentWithAuthenticationContext:self->_currentAction.authenticationContext]) { [self->_currentAction completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerRequiresAuthenticationContextErrorCode userInfo:nil]]; return; } - [presentingViewController presentViewController:safariViewController animated:YES completion:nil]; + STPVoidBlock doChallenge = ^{ + SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:url]; + safariViewController.delegate = self; + [presentingViewController presentViewController:safariViewController animated:YES completion:nil]; + }; + if ([self->_currentAction.authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]) { + [self->_currentAction.authenticationContext authenticationWillPresent:doChallenge]; + } else { + doChallenge(); + } }; if (@available(iOS 10, *)) { @@ -592,6 +602,28 @@ - (void)_handleRedirectToURL:(NSURL *)url withReturnURL:(nullable NSURL *)return } } +- (BOOL)_canPresentWithAuthenticationContext:(id)authenticationContext { + UIViewController *presentingViewController = authenticationContext.authenticationPresentingViewController; + // Is presentingViewController non-nil and in the window? + if (presentingViewController == nil || presentingViewController.view.window == nil) { + return NO; + } + + // Is it the Apple Pay VC? + if ([presentingViewController isKindOfClass:[PKPaymentAuthorizationViewController class]]) { + // We can't present over Apple Pay, user must implement authenticationWillPresent to dismiss it. + return [authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]; + } + + // Is it already presenting something? + if (presentingViewController.presentedViewController == nil) { + return YES; + } else { + // Hopefully the user implemented authenticationWillPresent: to dismiss it. + return [authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]; + } +} + #pragma mark - SFSafariViewControllerDelegate - (void)safariViewControllerDidFinish:(SFSafariViewController * __unused)controller { diff --git a/Stripe/PublicHeaders/STPAuthenticationContext.h b/Stripe/PublicHeaders/STPAuthenticationContext.h index 2987181f653..77c130ec415 100644 --- a/Stripe/PublicHeaders/STPAuthenticationContext.h +++ b/Stripe/PublicHeaders/STPAuthenticationContext.h @@ -8,6 +8,7 @@ #import #import +#import "STPBlocks.h" NS_ASSUME_NONNULL_BEGIN @@ -24,6 +25,17 @@ NS_ASSUME_NONNULL_BEGIN */ - (UIViewController *)authenticationPresentingViewController; +/** + This method is called before presenting a UIViewController for authentication. + + Implement this method if your customer is using Apple Pay. For security, it's impossible to present UIViewControllers above the Apple Pay sheet. + This method should dismiss the PKPaymentAuthorizationViewController and call `continueBlock` in the dismissal's completion block. + + Note that `paymentAuthorizationViewControllerDidFinish` is not called after `PKPaymentAuthorizationViewController` is dismissed. + */ +@optional +- (void)authenticationWillPresent:(STPVoidBlock)continueBlock; + @end NS_ASSUME_NONNULL_END diff --git a/Stripe/PublicHeaders/STPPaymentHandler.h b/Stripe/PublicHeaders/STPPaymentHandler.h index 864dfc98ddf..4f0a8c5f396 100644 --- a/Stripe/PublicHeaders/STPPaymentHandler.h +++ b/Stripe/PublicHeaders/STPPaymentHandler.h @@ -83,7 +83,8 @@ typedef NS_ENUM(NSInteger, STPPaymentHandlerErrorCode) { STPPaymentHandlerNoConcurrentActionsErrorCode, /** - Payment requires an `STPAuthenticationContext`. + Payment requires a valid `STPAuthenticationContext`. Make sure your presentingViewController isn't already presenting. + If you're using Apple Pay, you must implement `STPAuthenticationContext authenticationWillPresent` */ STPPaymentHandlerRequiresAuthenticationContextErrorCode, }; @@ -102,6 +103,10 @@ typedef void (^STPPaymentHandlerActionSetupIntentCompletionBlock)(STPPaymentHand /** `STPPaymentHandler` is a utility class that can confirm PaymentIntents and handle any additional required actions for 3DS(2) authentication. It can present authentication UI on top of your app or redirect users out of your app (to e.g. their banking app). + + @note If you're using Apple Pay, you must implement `STPAuthenticationContext authenticationWillPresent`. See that method's docstring for more details. + + @see https://stripe.com/docs/mobile/ios/authentication */ NS_EXTENSION_UNAVAILABLE("STPPaymentHandler is not available in extensions") @interface STPPaymentHandler : NSObject From 71a8096c937ab46c31031afcd33e02fd7cd9ae30 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 13:19:44 -0700 Subject: [PATCH 04/10] Conform STPPaymentContext to STPAuthenticationContext, implement authenticationWillPresent to dismiss apple pay, handle the dismissal case not calling paymentAuthorizationViewControllerDidFinish: --- ...uthorizationViewController+Stripe_Blocks.m | 12 +++++ Stripe/PublicHeaders/STPPaymentContext.h | 3 +- Stripe/STPPaymentContext.m | 53 +++++++++++++------ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m b/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m index bbee3c1ad84..e2a6af51cfe 100644 --- a/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m +++ b/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m @@ -48,10 +48,18 @@ - (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewC if (error) { self.lastError = error; completion(PKPaymentAuthorizationStatusFailure); + if (controller.presentingViewController == nil) { + // If we call completion() after dismissing, didFinishWithStatus is NOT called. + [self _finish]; + } return; } self.didSucceed = YES; completion(PKPaymentAuthorizationStatusSuccess); + if (controller.presentingViewController == nil) { + // If we call completion() after dismissing, didFinishWithStatus is NOT called. + [self _finish]; + } }); }; [self.apiClient createPaymentMethodWithPayment:payment completion:paymentMethodCreateCompletion]; @@ -80,6 +88,10 @@ - (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewC } - (void)paymentAuthorizationViewControllerDidFinish:(__unused PKPaymentAuthorizationViewController *)controller { + [self _finish]; +} + +- (void)_finish { if (self.didSucceed) { self.onFinish(STPPaymentStatusSuccess, nil); } diff --git a/Stripe/PublicHeaders/STPPaymentContext.h b/Stripe/PublicHeaders/STPPaymentContext.h index 2406eac38e9..8904fc21087 100644 --- a/Stripe/PublicHeaders/STPPaymentContext.h +++ b/Stripe/PublicHeaders/STPPaymentContext.h @@ -11,6 +11,7 @@ #import #import "STPAddress.h" +#import "STPAuthenticationContext.h" #import "STPBlocks.h" #import "STPPaymentConfiguration.h" #import "STPPaymentOption.h" @@ -29,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN `STPPaymentContext` saves information about a user's payment methods to a Stripe customer object, and requires an `STPCustomerContext` to manage retrieving and modifying the customer. */ -@interface STPPaymentContext : NSObject +@interface STPPaymentContext : NSObject /** This is a convenience initializer; it is equivalent to calling diff --git a/Stripe/STPPaymentContext.m b/Stripe/STPPaymentContext.m index 7c261ad2168..c85bfc4b00b 100644 --- a/Stripe/STPPaymentContext.m +++ b/Stripe/STPPaymentContext.m @@ -63,6 +63,7 @@ @interface STPPaymentContext() Date: Tue, 30 Jul 2019 13:49:42 -0700 Subject: [PATCH 05/10] Update Standard Integration example app --- .../Standard Integration/CheckoutViewController.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Example/Standard Integration/CheckoutViewController.swift b/Example/Standard Integration/CheckoutViewController.swift index 09b9d5698f2..7bc742c0bb7 100644 --- a/Example/Standard Integration/CheckoutViewController.swift +++ b/Example/Standard Integration/CheckoutViewController.swift @@ -9,7 +9,7 @@ import UIKit import Stripe -class CheckoutViewController: UIViewController, STPPaymentContextDelegate, STPAuthenticationContext { +class CheckoutViewController: UIViewController, STPPaymentContextDelegate { // 1) To get started with this demo, first head to https://dashboard.stripe.com/account/apikeys // and copy your "Test Publishable Key" (it looks like pk_test_abcdef) into the line below. @@ -231,11 +231,6 @@ See https://stripe.com/docs/testing. self.paymentContext.requestPayment() } - // MARK: STPAuthenticationContext - func authenticationPresentingViewController() -> UIViewController { - return self - } - // MARK: STPPaymentContextDelegate func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) { @@ -248,7 +243,7 @@ See https://stripe.com/docs/testing. completion(error ?? NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "Unable to parse clientSecret from response"])) return } - STPPaymentHandler.shared().handleNextAction(forPayment: clientSecret, with: self, returnURL: "payments-example://stripe-redirect") { (status, handledPaymentIntent, actionError) in + STPPaymentHandler.shared().handleNextAction(forPayment: clientSecret, with: paymentContext, returnURL: "payments-example://stripe-redirect") { (status, handledPaymentIntent, actionError) in switch (status) { case .succeeded: guard let handledPaymentIntent = handledPaymentIntent else { From 71bdf15909eaf12597503211b9641b433235343e Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 14:32:54 -0700 Subject: [PATCH 06/10] Update ApplePayExampleViewController --- .../ApplePayExampleViewController.m | 153 ++++++++++++------ 1 file changed, 102 insertions(+), 51 deletions(-) diff --git a/Example/Custom Integration/ApplePayExampleViewController.m b/Example/Custom Integration/ApplePayExampleViewController.m index f0d360635f4..e1aedc78081 100644 --- a/Example/Custom Integration/ApplePayExampleViewController.m +++ b/Example/Custom Integration/ApplePayExampleViewController.m @@ -19,11 +19,12 @@ that address. After the user submits their information, we create a token using the authorized PKPayment, and then send it to our backend to create the charge request. */ -@interface ApplePayExampleViewController () +@interface ApplePayExampleViewController () @property (nonatomic) ShippingManager *shippingManager; @property (nonatomic, weak) UIButton *payButton; @property (nonatomic) BOOL applePaySucceeded; @property (nonatomic) NSError *applePayError; +@property (nonatomic) PKPaymentAuthorizationViewController *applePayVC; @end @implementation ApplePayExampleViewController @@ -79,10 +80,11 @@ - (void)pay { PKPaymentRequest *paymentRequest = [self buildPaymentRequest]; if ([Stripe canSubmitPaymentRequest:paymentRequest]) { - PKPaymentAuthorizationViewController *auth = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest:paymentRequest]; - auth.delegate = self; - if (auth) { - [self presentViewController:auth animated:YES completion:nil]; + self.applePayVC = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest:paymentRequest]; + self.applePayVC.delegate = self; + + if (self.applePayVC) { + [self presentViewController:self.applePayVC animated:YES completion:nil]; } else { NSLog(@"Apple Pay returned a nil PKPaymentAuthorizationViewController - make sure you've configured Apple Pay correctly, as outlined at https://stripe.com/docs/mobile/apple-pay"); } @@ -118,66 +120,115 @@ - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller didAuthorizePayment:(PKPayment *)payment completion:(void (^)(PKPaymentAuthorizationStatus))completion { - [[STPAPIClient sharedClient] createTokenWithPayment:payment - completion:^(STPToken *token, NSError *error) { - if (error) { - self.applePayError = error; - completion(PKPaymentAuthorizationStatusFailure); - } else { - // We could also send the token.stripeID to our backend to create - // a payment method and subsequent payment intent - [self _createPaymentMethodForApplePayToken:token completion:completion]; - } - }]; -} - -- (void)_createPaymentMethodForApplePayToken:(STPToken *)token completion:(void (^)(PKPaymentAuthorizationStatus))completion { - STPPaymentMethodCardParams *applePayParams = [[STPPaymentMethodCardParams alloc] init]; - applePayParams.token = token.stripeID; - STPPaymentMethodParams *paymentMethodParams = [STPPaymentMethodParams paramsWithCard:applePayParams - billingDetails:nil - metadata:nil]; - - [[STPAPIClient sharedClient] createPaymentMethodWithParams:paymentMethodParams - completion:^(STPPaymentMethod * _Nullable paymentMethod, NSError * _Nullable error) { - if (error) { - self.applePayError = error; - completion(PKPaymentAuthorizationStatusFailure); - } else { - [self _createAndConfirmPaymentIntentWithPaymentMethod:paymentMethod - completion:completion]; - } - }]; + [[STPAPIClient sharedClient] createPaymentMethodWithPayment:payment completion:^(STPPaymentMethod *paymentMethod, NSError *error) { + if (error) { + self.applePayError = error; + completion(PKPaymentAuthorizationStatusFailure); + } else { + // We could also send the token.stripeID to our backend to create + // a payment method and subsequent payment intent + [self _createAndConfirmPaymentIntentWithPaymentMethod:paymentMethod + completion:completion]; + } + }]; } - (void)_createAndConfirmPaymentIntentWithPaymentMethod:(STPPaymentMethod *)paymentMethod completion:(void (^)(PKPaymentAuthorizationStatus))completion { + void (^reconfirmPaymentIntent)(STPPaymentIntent *) = ^(STPPaymentIntent *paymentIntent) { + [self.delegate confirmPaymentIntent:paymentIntent completion:^(STPBackendResult status, NSString *clientSecret, NSError *error) { + if (status == STPBackendResultFailure || error) { + self.applePayError = error; + self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + return; + } + [[STPAPIClient sharedClient] retrievePaymentIntentWithClientSecret:clientSecret completion:^(STPPaymentIntent *finalPaymentIntent, NSError *finalError) { + if (finalError) { + self.applePayError = finalError; + self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + return; + } + if (finalPaymentIntent.status == STPPaymentIntentStatusSucceeded || finalPaymentIntent.status == STPPaymentIntentStatusRequiresCapture) { + self.applePaySucceeded = YES; + self.applePayVC ? completion(PKPaymentAuthorizationStatusSuccess) : [self _finish]; + } else { + self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + } + }]; + }]; + }; + STPPaymentHandlerActionPaymentIntentCompletionBlock paymentHandlerCompletion = ^(STPPaymentHandlerActionStatus handlerStatus, STPPaymentIntent * _Nullable paymentIntent, NSError * _Nullable handlerError) { + switch (handlerStatus) { + case STPPaymentHandlerActionStatusFailed: + self.applePayError = handlerError; + self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + break; + case STPPaymentHandlerActionStatusCanceled: + self.applePayError = [NSError errorWithDomain:StripeDomain code:123 userInfo:@{NSLocalizedDescriptionKey: @"User cancelled"}]; + self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + break; + case STPPaymentHandlerActionStatusSucceeded: + if (paymentIntent.status == STPPaymentIntentStatusRequiresConfirmation) { + // Manually confirm the PaymentIntent on the backend again to complete the payment. + reconfirmPaymentIntent(paymentIntent); + break; + } else { + self.applePayVC ? completion(PKPaymentAuthorizationStatusSuccess) : [self _finish]; + } + } + }; + STPPaymentIntentCreateAndConfirmHandler createAndConfirmCompletion = ^(STPBackendResult status, NSString *clientSecret, NSError *error) { + if (status == STPBackendResultFailure || error) { + self.applePayError = error; + completion(PKPaymentAuthorizationStatusFailure); + return; + } + [[STPPaymentHandler sharedHandler] handleNextActionForPayment:clientSecret + withAuthenticationContext:self + returnURL:@"payments-example://stripe-redirect" + completion:paymentHandlerCompletion]; + }; [self.delegate createAndConfirmPaymentIntentWithAmount:@(1000) paymentMethod:paymentMethod.stripeId returnURL:@"payments-example://stripe-redirect" - completion:^(STPBackendResult status, NSString *clientSecret, NSError *error) { - if (error) { - self.applePayError = error; - completion(PKPaymentAuthorizationStatusFailure); - } else { - self.applePaySucceeded = YES; - completion(PKPaymentAuthorizationStatusSuccess); - } - }]; + completion:createAndConfirmCompletion]; } - (void)paymentAuthorizationViewControllerDidFinish:(PKPaymentAuthorizationViewController *)controller { dispatch_async(dispatch_get_main_queue(), ^{ + // This only gets called if you call the PKPaymentAuthorizationStatus completion block before dismissing PKPaymentAuthorizationViewController [self dismissViewControllerAnimated:YES completion:^{ - if (self.applePaySucceeded) { - [self.delegate exampleViewController:self didFinishWithMessage:@"Payment successfully created"]; - } else if (self.applePayError) { - [self.delegate exampleViewController:self didFinishWithError:self.applePayError]; - } - self.applePaySucceeded = NO; - self.applePayError = nil; + [self _finish]; }]; }); } +- (void)_finish { + if (self.applePaySucceeded) { + [self.delegate exampleViewController:self didFinishWithMessage:@"Payment successfully created"]; + } else if (self.applePayError) { + [self.delegate exampleViewController:self didFinishWithError:self.applePayError]; + } + self.applePaySucceeded = NO; + self.applePayError = nil; + self.applePayVC = nil; +} + +#pragma mark - STPAuthenticationContext + +- (UIViewController *)authenticationPresentingViewController { + return self; +} + +- (void)authenticationWillPresent:(STPVoidBlock)continueBlock { + if (self.applePayVC.presentingViewController != nil) { + [self dismissViewControllerAnimated:YES completion:^{ + self.applePayVC = nil; + continueBlock(); + }]; + } else { + continueBlock(); + } +} + @end From dc08018522702e4ae10547e36f2d54e2fa3d7575 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 14:55:33 -0700 Subject: [PATCH 07/10] Make sure hostViewController is non-nil before attempting Apple Pay in STPPaymentContext --- Stripe/STPPaymentContext.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Stripe/STPPaymentContext.m b/Stripe/STPPaymentContext.m index c85bfc4b00b..d0e1a4aa69d 100644 --- a/Stripe/STPPaymentContext.m +++ b/Stripe/STPPaymentContext.m @@ -607,6 +607,7 @@ - (void)requestPayment { }]; } else if ([self.selectedPaymentOption isKindOfClass:[STPApplePayPaymentOption class]]) { + NSCAssert(self.hostViewController != nil, @"hostViewController must not be nil on STPPaymentContext. Next time, set the hostViewController property first!"); self.state = STPPaymentContextStateRequestingPayment; PKPaymentRequest *paymentRequest = [self buildPaymentRequest]; STPShippingAddressSelectionBlock shippingAddressHandler = ^(STPAddress *shippingAddress, STPShippingAddressValidationBlock completion) { From 12a624e0196e2b9286cf2598523ec04c99643b9e Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 15:18:14 -0700 Subject: [PATCH 08/10] Fix fauxpas, jazzy issue --- Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m | 2 +- Stripe/PublicHeaders/STPAuthenticationContext.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m b/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m index e2a6af51cfe..13e3700fbcb 100644 --- a/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m +++ b/Stripe/PKPaymentAuthorizationViewController+Stripe_Blocks.m @@ -34,7 +34,7 @@ @interface STPBlockBasedApplePayDelegate : NSObject Date: Tue, 30 Jul 2019 16:42:30 -0700 Subject: [PATCH 09/10] PR feedback --- .../ApplePayExampleViewController.m | 27 ++++++++++++------- Stripe/Payments/STPPaymentHandler.m | 16 +++++------ .../PublicHeaders/STPAuthenticationContext.h | 2 +- Stripe/PublicHeaders/STPPaymentHandler.h | 4 +-- Stripe/STPPaymentContext.m | 6 ++--- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/Example/Custom Integration/ApplePayExampleViewController.m b/Example/Custom Integration/ApplePayExampleViewController.m index e1aedc78081..4bf6b7f388b 100644 --- a/Example/Custom Integration/ApplePayExampleViewController.m +++ b/Example/Custom Integration/ApplePayExampleViewController.m @@ -134,24 +134,31 @@ - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController } - (void)_createAndConfirmPaymentIntentWithPaymentMethod:(STPPaymentMethod *)paymentMethod completion:(void (^)(PKPaymentAuthorizationStatus))completion { + void (^finishWithStatus)(PKPaymentAuthorizationStatus) = ^(PKPaymentAuthorizationStatus status) { + if (self.applePayVC) { + completion(status); + } else { + [self _finish]; + } + }; void (^reconfirmPaymentIntent)(STPPaymentIntent *) = ^(STPPaymentIntent *paymentIntent) { [self.delegate confirmPaymentIntent:paymentIntent completion:^(STPBackendResult status, NSString *clientSecret, NSError *error) { if (status == STPBackendResultFailure || error) { self.applePayError = error; - self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusFailure); return; } [[STPAPIClient sharedClient] retrievePaymentIntentWithClientSecret:clientSecret completion:^(STPPaymentIntent *finalPaymentIntent, NSError *finalError) { if (finalError) { self.applePayError = finalError; - self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusFailure); return; } if (finalPaymentIntent.status == STPPaymentIntentStatusSucceeded || finalPaymentIntent.status == STPPaymentIntentStatusRequiresCapture) { self.applePaySucceeded = YES; - self.applePayVC ? completion(PKPaymentAuthorizationStatusSuccess) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusSuccess); } else { - self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusFailure); } }]; }]; @@ -160,11 +167,11 @@ - (void)_createAndConfirmPaymentIntentWithPaymentMethod:(STPPaymentMethod *)paym switch (handlerStatus) { case STPPaymentHandlerActionStatusFailed: self.applePayError = handlerError; - self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusFailure); break; case STPPaymentHandlerActionStatusCanceled: self.applePayError = [NSError errorWithDomain:StripeDomain code:123 userInfo:@{NSLocalizedDescriptionKey: @"User cancelled"}]; - self.applePayVC ? completion(PKPaymentAuthorizationStatusFailure) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusFailure); break; case STPPaymentHandlerActionStatusSucceeded: if (paymentIntent.status == STPPaymentIntentStatusRequiresConfirmation) { @@ -172,7 +179,7 @@ - (void)_createAndConfirmPaymentIntentWithPaymentMethod:(STPPaymentMethod *)paym reconfirmPaymentIntent(paymentIntent); break; } else { - self.applePayVC ? completion(PKPaymentAuthorizationStatusSuccess) : [self _finish]; + finishWithStatus(PKPaymentAuthorizationStatusSuccess); } } }; @@ -220,14 +227,14 @@ - (UIViewController *)authenticationPresentingViewController { return self; } -- (void)authenticationWillPresent:(STPVoidBlock)continueBlock { +- (void)prepareAuthenticationContextForPresentation:(STPVoidBlock)completion { if (self.applePayVC.presentingViewController != nil) { [self dismissViewControllerAnimated:YES completion:^{ self.applePayVC = nil; - continueBlock(); + completion(); }]; } else { - continueBlock(); + completion(); } } diff --git a/Stripe/Payments/STPPaymentHandler.m b/Stripe/Payments/STPPaymentHandler.m index 1c64567e9ab..6c0915d6e03 100644 --- a/Stripe/Payments/STPPaymentHandler.m +++ b/Stripe/Payments/STPPaymentHandler.m @@ -488,8 +488,8 @@ - (void)_handleAuthenticationForCurrentAction { [self->_currentAction completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerStripe3DS2ErrorCode userInfo:@{@"exception": exception}]]; } }; - if ([self->_currentAction.authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]) { - [self->_currentAction.authenticationContext authenticationWillPresent:doChallenge]; + if ([self->_currentAction.authenticationContext respondsToSelector:@selector(prepareAuthenticationContextForPresentation:)]) { + [self->_currentAction.authenticationContext prepareAuthenticationContextForPresentation:doChallenge]; } else { doChallenge(); } @@ -576,8 +576,8 @@ - (void)_handleRedirectToURL:(NSURL *)url withReturnURL:(nullable NSURL *)return safariViewController.delegate = self; [presentingViewController presentViewController:safariViewController animated:YES completion:nil]; }; - if ([self->_currentAction.authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]) { - [self->_currentAction.authenticationContext authenticationWillPresent:doChallenge]; + if ([self->_currentAction.authenticationContext respondsToSelector:@selector(prepareAuthenticationContextForPresentation:)]) { + [self->_currentAction.authenticationContext prepareAuthenticationContextForPresentation:doChallenge]; } else { doChallenge(); } @@ -611,16 +611,16 @@ - (BOOL)_canPresentWithAuthenticationContext:(id)authe // Is it the Apple Pay VC? if ([presentingViewController isKindOfClass:[PKPaymentAuthorizationViewController class]]) { - // We can't present over Apple Pay, user must implement authenticationWillPresent to dismiss it. - return [authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]; + // We can't present over Apple Pay, user must implement prepareAuthenticationContextForPresentation: to dismiss it. + return [authenticationContext respondsToSelector:@selector(prepareAuthenticationContextForPresentation:)]; } // Is it already presenting something? if (presentingViewController.presentedViewController == nil) { return YES; } else { - // Hopefully the user implemented authenticationWillPresent: to dismiss it. - return [authenticationContext respondsToSelector:@selector(authenticationWillPresent:)]; + // Hopefully the user implemented prepareAuthenticationContextForPresentation: to dismiss it. + return [authenticationContext respondsToSelector:@selector(prepareAuthenticationContextForPresentation:)]; } } diff --git a/Stripe/PublicHeaders/STPAuthenticationContext.h b/Stripe/PublicHeaders/STPAuthenticationContext.h index 9d3bf69e2a5..42a9efee303 100644 --- a/Stripe/PublicHeaders/STPAuthenticationContext.h +++ b/Stripe/PublicHeaders/STPAuthenticationContext.h @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN Note that `paymentAuthorizationViewControllerDidFinish` is not called after `PKPaymentAuthorizationViewController` is dismissed. */ -- (void)authenticationWillPresent:(STPVoidBlock)continueBlock; +- (void)prepareAuthenticationContextForPresentation:(STPVoidBlock)completion; @end diff --git a/Stripe/PublicHeaders/STPPaymentHandler.h b/Stripe/PublicHeaders/STPPaymentHandler.h index 4f0a8c5f396..f052cec846a 100644 --- a/Stripe/PublicHeaders/STPPaymentHandler.h +++ b/Stripe/PublicHeaders/STPPaymentHandler.h @@ -84,7 +84,7 @@ typedef NS_ENUM(NSInteger, STPPaymentHandlerErrorCode) { /** Payment requires a valid `STPAuthenticationContext`. Make sure your presentingViewController isn't already presenting. - If you're using Apple Pay, you must implement `STPAuthenticationContext authenticationWillPresent` + If you're using Apple Pay, you must implement `STPAuthenticationContext prepareAuthenticationContextForPresentation:` */ STPPaymentHandlerRequiresAuthenticationContextErrorCode, }; @@ -104,7 +104,7 @@ typedef void (^STPPaymentHandlerActionSetupIntentCompletionBlock)(STPPaymentHand `STPPaymentHandler` is a utility class that can confirm PaymentIntents and handle any additional required actions for 3DS(2) authentication. It can present authentication UI on top of your app or redirect users out of your app (to e.g. their banking app). - @note If you're using Apple Pay, you must implement `STPAuthenticationContext authenticationWillPresent`. See that method's docstring for more details. + @note If you're using Apple Pay, you must implement `STPAuthenticationContext prepareAuthenticationContextForPresentation:`. See that method's docstring for more details. @see https://stripe.com/docs/mobile/ios/authentication */ diff --git a/Stripe/STPPaymentContext.m b/Stripe/STPPaymentContext.m index d0e1a4aa69d..fb39c5a5fc2 100644 --- a/Stripe/STPPaymentContext.m +++ b/Stripe/STPPaymentContext.m @@ -752,14 +752,14 @@ - (UIViewController *)authenticationPresentingViewController { return self.hostViewController; } -- (void)authenticationWillPresent:(STPVoidBlock)continueBlock { +- (void)prepareAuthenticationContextForPresentation:(STPVoidBlock)completion { if (self.applePayVC && self.applePayVC.presentingViewController != nil) { [self.hostViewController dismissViewControllerAnimated:[self transitionAnimationsEnabled] completion:^{ - continueBlock(); + completion(); }]; } else { - continueBlock(); + completion(); } } From 4de694c34b5727c61119214f5a1f328fa591c04a Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Tue, 30 Jul 2019 17:07:19 -0700 Subject: [PATCH 10/10] PR feedback --- Stripe/PublicHeaders/STPAuthenticationContext.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Stripe/PublicHeaders/STPAuthenticationContext.h b/Stripe/PublicHeaders/STPAuthenticationContext.h index 42a9efee303..b1929ed3a81 100644 --- a/Stripe/PublicHeaders/STPAuthenticationContext.h +++ b/Stripe/PublicHeaders/STPAuthenticationContext.h @@ -30,9 +30,10 @@ NS_ASSUME_NONNULL_BEGIN This method is called before presenting a UIViewController for authentication. Implement this method if your customer is using Apple Pay. For security, it's impossible to present UIViewControllers above the Apple Pay sheet. - This method should dismiss the PKPaymentAuthorizationViewController and call `continueBlock` in the dismissal's completion block. + This method should dismiss the PKPaymentAuthorizationViewController and call `completion` in the dismissal's completion block. - Note that `paymentAuthorizationViewControllerDidFinish` is not called after `PKPaymentAuthorizationViewController` is dismissed. + @note `STPPaymentHandler` will not proceed until `completion` is called. + @note `paymentAuthorizationViewControllerDidFinish` is not called after `PKPaymentAuthorizationViewController` is dismissed. */ - (void)prepareAuthenticationContextForPresentation:(STPVoidBlock)completion;