Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time. Cannot retrieve contributors at this time
755 lines (678 sloc) 36.7 KB
//
// STPPaymentContext.m
// Stripe
//
// Created by Jack Flintermann on 4/20/16.
// Copyright © 2016 Stripe, Inc. All rights reserved.
//
#import <PassKit/PassKit.h>
#import <objc/runtime.h>
#import "PKPaymentAuthorizationViewController+Stripe_Blocks.h"
#import "STPAddCardViewController+Private.h"
#import "STPCustomer+SourceTuple.h"
#import "STPCustomerContext.h"
#import "STPDispatchFunctions.h"
#import "STPPaymentConfiguration+Private.h"
#import "STPPaymentContext+Private.h"
#import "STPPaymentContextAmountModel.h"
#import "STPPaymentMethodTuple.h"
#import "STPPromise.h"
#import "STPShippingMethodsViewController.h"
#import "STPWeakStrongMacros.h"
#import "UINavigationController+Stripe_Completion.h"
#import "UIViewController+Stripe_ParentViewController.h"
#import "UIViewController+Stripe_Promises.h"
/**
The current state of the payment context
- STPPaymentContextStateNone: No view controllers are currently being shown. The payment may or may not have already been completed
- STPPaymentContextStateShowingRequestedViewController: The view controller that you requested the context show is being shown (via the push or present payment methods or shipping view controller methods)
- STPPaymentContextStateRequestingPayment: The payment context is in the middle of requesting payment. It may be showing some other UI or view controller if more information is necessary to complete the payment.
*/
typedef NS_ENUM(NSUInteger, STPPaymentContextState) {
STPPaymentContextStateNone,
STPPaymentContextStateShowingRequestedViewController,
STPPaymentContextStateRequestingPayment,
};
@interface STPPaymentContext()<STPPaymentMethodsViewControllerDelegate, STPShippingAddressViewControllerDelegate>
@property (nonatomic) STPPaymentConfiguration *configuration;
@property (nonatomic) STPTheme *theme;
@property (nonatomic) id<STPBackendAPIAdapter> apiAdapter;
@property (nonatomic) STPAPIClient *apiClient;
@property (nonatomic) STPPromise<STPPaymentMethodTuple *> *loadingPromise;
// these wrap hostViewController's promises because the hostVC is nil at init-time
@property (nonatomic) STPVoidPromise *willAppearPromise;
@property (nonatomic) STPVoidPromise *didAppearPromise;
@property (nonatomic, weak) STPPaymentMethodsViewController *paymentMethodsViewController;
@property (nonatomic) id<STPPaymentMethod> selectedPaymentMethod;
@property (nonatomic) NSArray<id<STPPaymentMethod>> *paymentMethods;
@property (nonatomic) STPAddress *shippingAddress;
@property (nonatomic) PKShippingMethod *selectedShippingMethod;
@property (nonatomic) NSArray<PKShippingMethod *> *shippingMethods;
@property (nonatomic, assign) STPPaymentContextState state;
@property (nonatomic) STPPaymentContextAmountModel *paymentAmountModel;
@property (nonatomic) BOOL shippingAddressNeedsVerification;
// If hostViewController was set to a nav controller, the original VC on top of the stack
@property (nonatomic, weak) UIViewController *originalTopViewController;
@end
@implementation STPPaymentContext
- (instancetype)initWithCustomerContext:(STPCustomerContext *)customerContext {
return [self initWithAPIAdapter:customerContext];
}
- (instancetype)initWithCustomerContext:(STPCustomerContext *)customerContext
configuration:(STPPaymentConfiguration *)configuration
theme:(STPTheme *)theme {
return [self initWithAPIAdapter:customerContext
configuration:configuration
theme:theme];
}
- (instancetype)initWithAPIAdapter:(id<STPBackendAPIAdapter>)apiAdapter {
return [self initWithAPIAdapter:apiAdapter
configuration:[STPPaymentConfiguration sharedConfiguration]
theme:[STPTheme defaultTheme]];
}
- (instancetype)initWithAPIAdapter:(id<STPBackendAPIAdapter>)apiAdapter
configuration:(STPPaymentConfiguration *)configuration
theme:(STPTheme *)theme {
self = [super init];
if (self) {
_configuration = configuration;
_apiAdapter = apiAdapter;
_theme = theme;
_willAppearPromise = [STPVoidPromise new];
_didAppearPromise = [STPVoidPromise new];
_apiClient = [[STPAPIClient alloc] initWithPublishableKey:configuration.publishableKey];
_paymentCurrency = @"USD";
_paymentCountry = @"US";
_paymentAmountModel = [[STPPaymentContextAmountModel alloc] initWithAmount:0];
_modalPresentationStyle = UIModalPresentationFullScreen;
if (@available(iOS 11, *)) {
_largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeAutomatic;
}
_state = STPPaymentContextStateNone;
[self retryLoading];
}
return self;
}
- (void)retryLoading {
// Clear any cached customer object before refetching
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
STPCustomerContext *customerContext = (STPCustomerContext *)self.apiAdapter;
[customerContext clearCachedCustomer];
}
WEAK(self);
self.loadingPromise = [[[STPPromise<STPPaymentMethodTuple *> new] onSuccess:^(STPPaymentMethodTuple *tuple) {
STRONG(self);
self.paymentMethods = tuple.paymentMethods;
self.selectedPaymentMethod = tuple.selectedPaymentMethod;
}] onFailure:^(NSError * _Nonnull error) {
STRONG(self);
if (self.hostViewController) {
[self.didAppearPromise onSuccess:^(__unused id value) {
if (self.paymentMethodsViewController) {
[self appropriatelyDismissPaymentMethodsViewController:self.paymentMethodsViewController completion:^{
[self.delegate paymentContext:self didFailToLoadWithError:error];
}];
} else {
[self.delegate paymentContext:self didFailToLoadWithError:error];
}
}];
}
}];
[self.apiAdapter retrieveCustomer:^(STPCustomer * _Nullable customer, NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
STRONG(self);
if (!self) {
return;
}
if (error) {
[self.loadingPromise fail:error];
return;
}
if (!self.shippingAddress && customer.shippingAddress) {
self.shippingAddress = customer.shippingAddress;
self.shippingAddressNeedsVerification = YES;
}
STPPaymentMethodTuple *paymentTuple = [customer filteredSourceTupleForUIWithConfiguration:self.configuration];
[self.loadingPromise succeed:paymentTuple];
});
}];
}
- (BOOL)loading {
return !self.loadingPromise.completed;
}
// Disable transition animations in tests
- (BOOL)transitionAnimationsEnabled {
return NSClassFromString(@"XCTest") == nil;
}
- (void)setHostViewController:(UIViewController *)hostViewController {
NSCAssert(_hostViewController == nil, @"You cannot change the hostViewController on an STPPaymentContext after it's already been set.");
_hostViewController = hostViewController;
if ([hostViewController isKindOfClass:[UINavigationController class]]) {
self.originalTopViewController = ((UINavigationController *)hostViewController).topViewController;
}
[self artificiallyRetain:hostViewController];
[self.willAppearPromise voidCompleteWith:hostViewController.stp_willAppearPromise];
[self.didAppearPromise voidCompleteWith:hostViewController.stp_didAppearPromise];
}
- (void)setDelegate:(id<STPPaymentContextDelegate>)delegate {
_delegate = delegate;
WEAK(self);
[self.willAppearPromise voidOnSuccess:^{
STRONG(self);
if (self.delegate == delegate) {
[delegate paymentContextDidChange:self];
}
}];
}
- (STPPromise<STPPaymentMethodTuple *> *)currentValuePromise {
WEAK(self);
return (STPPromise<STPPaymentMethodTuple *> *)[self.loadingPromise map:^id _Nonnull(__unused STPPaymentMethodTuple *value) {
STRONG(self);
return [STPPaymentMethodTuple tupleWithPaymentMethods:self.paymentMethods
selectedPaymentMethod:self.selectedPaymentMethod];
}];
}
- (void)setPrefilledInformation:(STPUserInformation *)prefilledInformation {
_prefilledInformation = prefilledInformation;
if (prefilledInformation.shippingAddress && !self.shippingAddress) {
self.shippingAddress = prefilledInformation.shippingAddress;
self.shippingAddressNeedsVerification = YES;
}
}
- (void)setPaymentMethods:(NSArray<id<STPPaymentMethod>> *)paymentMethods {
_paymentMethods = [paymentMethods sortedArrayUsingComparator:^NSComparisonResult(id<STPPaymentMethod> obj1, id<STPPaymentMethod> obj2) {
Class applePayKlass = [STPApplePayPaymentMethod class];
Class cardKlass = [STPCard class];
if ([obj1 isKindOfClass:applePayKlass]) {
return NSOrderedAscending;
} else if ([obj2 isKindOfClass:applePayKlass]) {
return NSOrderedDescending;
}
if ([obj1 isKindOfClass:cardKlass] && [obj2 isKindOfClass:cardKlass]) {
return [[((STPCard *)obj1) label]
compare:[((STPCard *)obj2) label]];
}
return NSOrderedSame;
}];
}
- (void)setSelectedPaymentMethod:(id<STPPaymentMethod>)selectedPaymentMethod {
if (selectedPaymentMethod && ![self.paymentMethods containsObject:selectedPaymentMethod]) {
self.paymentMethods = [self.paymentMethods arrayByAddingObject:selectedPaymentMethod];
}
if (![_selectedPaymentMethod isEqual:selectedPaymentMethod]) {
_selectedPaymentMethod = selectedPaymentMethod;
stpDispatchToMainThreadIfNecessary(^{
[self.delegate paymentContextDidChange:self];
});
}
}
- (void)setPaymentAmount:(NSInteger)paymentAmount {
self.paymentAmountModel = [[STPPaymentContextAmountModel alloc] initWithAmount:paymentAmount];
}
- (NSInteger)paymentAmount {
return [self.paymentAmountModel paymentAmountWithCurrency:self.paymentCurrency
shippingMethod:self.selectedShippingMethod];
}
- (void)setPaymentSummaryItems:(NSArray<PKPaymentSummaryItem *> *)paymentSummaryItems {
self.paymentAmountModel = [[STPPaymentContextAmountModel alloc] initWithPaymentSummaryItems:paymentSummaryItems];
}
- (NSArray<PKPaymentSummaryItem *> *)paymentSummaryItems {
return [self.paymentAmountModel paymentSummaryItemsWithCurrency:self.paymentCurrency
companyName:self.configuration.companyName
shippingMethod:self.selectedShippingMethod];
}
- (void)setShippingMethods:(NSArray<PKShippingMethod *> *)shippingMethods {
_shippingMethods = shippingMethods;
if (shippingMethods != nil && self.selectedShippingMethod != nil) {
if ([shippingMethods count] == 0) {
self.selectedShippingMethod = nil;
}
else if ([shippingMethods indexOfObject:self.selectedShippingMethod] == NSNotFound) {
self.selectedShippingMethod = [shippingMethods firstObject];
}
}
}
- (void)removePaymentMethod:(id<STPPaymentMethod>)paymentMethodToRemove {
// Remove payment method from cached representation
NSMutableArray *paymentMethods = [self.paymentMethods mutableCopy];
[paymentMethods removeObject:paymentMethodToRemove];
self.paymentMethods = paymentMethods;
// Elect new selected payment method if needed
if ([self.selectedPaymentMethod isEqual:paymentMethodToRemove]) {
self.selectedPaymentMethod = self.paymentMethods.firstObject;
}
}
#pragma mark - Payment Methods
- (void)presentPaymentMethodsViewController {
[self presentPaymentMethodsViewControllerWithNewState:STPPaymentContextStateShowingRequestedViewController];
}
- (void)presentPaymentMethodsViewControllerWithNewState:(STPPaymentContextState)state {
NSCAssert(self.hostViewController != nil, @"hostViewController must not be nil on STPPaymentContext when calling pushPaymentMethodsViewController on it. Next time, set the hostViewController property first!");
WEAK(self);
[self.didAppearPromise voidOnSuccess:^{
STRONG(self);
if (self.state == STPPaymentContextStateNone) {
self.state = state;
STPPaymentMethodsViewController *paymentMethodsViewController = [[STPPaymentMethodsViewController alloc] initWithPaymentContext:self];
self.paymentMethodsViewController = paymentMethodsViewController;
paymentMethodsViewController.prefilledInformation = self.prefilledInformation;
paymentMethodsViewController.paymentMethodsViewControllerFooterView = self.paymentMethodsViewControllerFooterView;
paymentMethodsViewController.addCardViewControllerFooterView = self.addCardViewControllerFooterView;
if (@available(iOS 11, *)) {
paymentMethodsViewController.navigationItem.largeTitleDisplayMode = self.largeTitleDisplayMode;
}
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:paymentMethodsViewController];
navigationController.navigationBar.stp_theme = self.theme;
if (@available(iOS 11, *)) {
navigationController.navigationBar.prefersLargeTitles = YES;
}
navigationController.modalPresentationStyle = self.modalPresentationStyle;
[self.hostViewController presentViewController:navigationController
animated:[self transitionAnimationsEnabled]
completion:nil];
}
}];
}
- (void)pushPaymentMethodsViewController {
NSCAssert(self.hostViewController != nil, @"hostViewController must not be nil on STPPaymentContext when calling pushPaymentMethodsViewController on it. Next time, set the hostViewController property first!");
UINavigationController *navigationController;
if ([self.hostViewController isKindOfClass:[UINavigationController class]]) {
navigationController = (UINavigationController *)self.hostViewController;
} else {
navigationController = self.hostViewController.navigationController;
}
NSCAssert(self.hostViewController != nil, @"The payment context's hostViewController is not a navigation controller, or is not contained in one. Either make sure it is inside a navigation controller before calling pushPaymentMethodsViewController, or call presentPaymentMethodsViewController instead.");
WEAK(self);
[self.didAppearPromise voidOnSuccess:^{
STRONG(self);
if (self.state == STPPaymentContextStateNone) {
self.state = STPPaymentContextStateShowingRequestedViewController;
STPPaymentMethodsViewController *paymentMethodsViewController = [[STPPaymentMethodsViewController alloc] initWithPaymentContext:self];
self.paymentMethodsViewController = paymentMethodsViewController;
paymentMethodsViewController.prefilledInformation = self.prefilledInformation;
paymentMethodsViewController.paymentMethodsViewControllerFooterView = self.paymentMethodsViewControllerFooterView;
paymentMethodsViewController.addCardViewControllerFooterView = self.addCardViewControllerFooterView;
if (@available(iOS 11, *)) {
paymentMethodsViewController.navigationItem.largeTitleDisplayMode = self.largeTitleDisplayMode;
}
[navigationController pushViewController:paymentMethodsViewController
animated:[self transitionAnimationsEnabled]];
}
}];
}
- (void)paymentMethodsViewController:(__unused STPPaymentMethodsViewController *)paymentMethodsViewController
didSelectPaymentMethod:(id<STPPaymentMethod>)paymentMethod {
self.selectedPaymentMethod = paymentMethod;
}
- (void)paymentMethodsViewControllerDidFinish:(STPPaymentMethodsViewController *)paymentMethodsViewController {
[self appropriatelyDismissPaymentMethodsViewController:paymentMethodsViewController completion:^{
if (self.state == STPPaymentContextStateRequestingPayment) {
self.state = STPPaymentContextStateNone;
[self requestPayment];
}
else {
self.state = STPPaymentContextStateNone;
}
}];
}
- (void)paymentMethodsViewControllerDidCancel:(STPPaymentMethodsViewController *)paymentMethodsViewController {
[self appropriatelyDismissPaymentMethodsViewController:paymentMethodsViewController completion:^{
if (self.state == STPPaymentContextStateRequestingPayment) {
[self didFinishWithStatus:STPPaymentStatusUserCancellation
error:nil];
}
else {
self.state = STPPaymentContextStateNone;
}
}];
}
- (void)paymentMethodsViewController:(__unused STPPaymentMethodsViewController *)paymentMethodsViewController
didFailToLoadWithError:(__unused NSError *)error {
// we'll handle this ourselves when the loading promise fails.
}
- (void)appropriatelyDismissPaymentMethodsViewController:(STPPaymentMethodsViewController *)viewController
completion:(STPVoidBlock)completion {
if ([viewController stp_isAtRootOfNavigationController]) {
// if we're the root of the navigation controller, we've been presented modally.
[viewController.presentingViewController dismissViewControllerAnimated:[self transitionAnimationsEnabled]
completion:^{
self.paymentMethodsViewController = nil;
if (completion) {
completion();
}
}];
} else {
// otherwise, we've been pushed onto the stack.
UIViewController *destinationViewController = self.hostViewController;
// If hostViewController is a nav controller, pop to the original VC on top of the stack.
if ([self.hostViewController isKindOfClass:[UINavigationController class]]) {
destinationViewController = self.originalTopViewController;
}
[viewController.navigationController stp_popToViewController:destinationViewController
animated:[self transitionAnimationsEnabled]
completion:^{
self.paymentMethodsViewController = nil;
if (completion) {
completion();
}
}];
}
}
#pragma mark - Shipping Info
- (void)presentShippingViewController {
[self presentShippingViewControllerWithNewState:STPPaymentContextStateShowingRequestedViewController];
}
- (void)presentShippingViewControllerWithNewState:(STPPaymentContextState)state {
NSCAssert(self.hostViewController != nil, @"hostViewController must not be nil on STPPaymentContext when calling presentShippingViewController on it. Next time, set the hostViewController property first!");
WEAK(self);
[self.didAppearPromise voidOnSuccess:^{
STRONG(self);
if (self.state == STPPaymentContextStateNone) {
self.state = state;
STPShippingAddressViewController *addressViewController = [[STPShippingAddressViewController alloc] initWithPaymentContext:self];
if (@available(iOS 11, *)) {
addressViewController.navigationItem.largeTitleDisplayMode = self.largeTitleDisplayMode;
}
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addressViewController];
navigationController.navigationBar.stp_theme = self.theme;
if (@available(iOS 11, *)) {
navigationController.navigationBar.prefersLargeTitles = YES;
}
navigationController.modalPresentationStyle = self.modalPresentationStyle;
[self.hostViewController presentViewController:navigationController
animated:[self transitionAnimationsEnabled]
completion:nil];
}
}];
}
- (void)pushShippingViewController {
NSCAssert(self.hostViewController != nil, @"hostViewController must not be nil on STPPaymentContext when calling pushShippingViewController on it. Next time, set the hostViewController property first!");
UINavigationController *navigationController;
if ([self.hostViewController isKindOfClass:[UINavigationController class]]) {
navigationController = (UINavigationController *)self.hostViewController;
} else {
navigationController = self.hostViewController.navigationController;
}
NSCAssert(self.hostViewController != nil, @"The payment context's hostViewController is not a navigation controller, or is not contained in one. Either make sure it is inside a navigation controller before calling pushShippingInfoViewController, or call presentShippingInfoViewController instead.");
WEAK(self);
[self.didAppearPromise voidOnSuccess:^{
STRONG(self);
if (self.state == STPPaymentContextStateNone) {
self.state = STPPaymentContextStateShowingRequestedViewController;
STPShippingAddressViewController *addressViewController = [[STPShippingAddressViewController alloc] initWithPaymentContext:self];
if (@available(iOS 11, *)) {
addressViewController.navigationItem.largeTitleDisplayMode = self.largeTitleDisplayMode;
}
[navigationController pushViewController:addressViewController
animated:[self transitionAnimationsEnabled]];
}
}];
}
- (void)shippingAddressViewControllerDidCancel:(STPShippingAddressViewController *)addressViewController {
[self appropriatelyDismissViewController:addressViewController completion:^{
if (self.state == STPPaymentContextStateRequestingPayment) {
[self didFinishWithStatus:STPPaymentStatusUserCancellation
error:nil];
}
else {
self.state = STPPaymentContextStateNone;
}
}];
}
- (void)shippingAddressViewController:(__unused STPShippingAddressViewController *)addressViewController
didEnterAddress:(STPAddress *)address
completion:(STPShippingMethodsCompletionBlock)completion {
if ([self.delegate respondsToSelector:@selector(paymentContext:didUpdateShippingAddress:completion:)]) {
[self.delegate paymentContext:self didUpdateShippingAddress:address completion:^(STPShippingStatus status, NSError *shippingValidationError, NSArray<PKShippingMethod *> * shippingMethods, PKShippingMethod *selectedMethod) {
self.shippingMethods = shippingMethods;
if (completion) {
completion(status, shippingValidationError, shippingMethods, selectedMethod);
}
}];
}
else {
if (completion) {
completion(STPShippingStatusValid, nil, nil, nil);
}
}
}
- (void)shippingAddressViewController:(STPShippingAddressViewController *)addressViewController
didFinishWithAddress:(STPAddress *)address
shippingMethod:(PKShippingMethod *)method {
self.shippingAddress = address;
self.shippingAddressNeedsVerification = NO;
self.selectedShippingMethod = method;
[self.delegate paymentContextDidChange:self];
if ([self.apiAdapter respondsToSelector:@selector(updateCustomerWithShippingAddress:completion:)]) {
[self.apiAdapter updateCustomerWithShippingAddress:self.shippingAddress completion:nil];
}
[self appropriatelyDismissViewController:addressViewController completion:^{
if (self.state == STPPaymentContextStateRequestingPayment) {
self.state = STPPaymentContextStateNone;
[self requestPayment];
} else {
self.state = STPPaymentContextStateNone;
}
}];
}
- (void)appropriatelyDismissViewController:(UIViewController *)viewController
completion:(STPVoidBlock)completion {
if ([viewController stp_isAtRootOfNavigationController]) {
// if we're the root of the navigation controller, we've been presented modally.
[viewController.presentingViewController dismissViewControllerAnimated:[self transitionAnimationsEnabled]
completion:^{
if (completion) {
completion();
}
}];
} else {
// otherwise, we've been pushed onto the stack.
UIViewController *destinationViewController = self.hostViewController;
// If hostViewController is a nav controller, pop to the original VC on top of the stack.
if ([self.hostViewController isKindOfClass:[UINavigationController class]]) {
destinationViewController = self.originalTopViewController;
}
[viewController.navigationController stp_popToViewController:destinationViewController
animated:[self transitionAnimationsEnabled]
completion:^{
if (completion) {
completion();
}
}];
}
}
#pragma mark - Request Payment
- (BOOL)requestPaymentShouldPresentShippingViewController {
BOOL shippingAddressRequired = self.configuration.requiredShippingAddressFields.count > 0;
BOOL shippingAddressIncomplete = ![self.shippingAddress containsRequiredShippingAddressFields:self.configuration.requiredShippingAddressFields];
BOOL shippingMethodRequired = (self.configuration.shippingType == STPShippingTypeShipping &&
[self.delegate respondsToSelector:@selector(paymentContext:didUpdateShippingAddress:completion:)] &&
!self.selectedShippingMethod);
BOOL verificationRequired = self.configuration.verifyPrefilledShippingAddress && self.shippingAddressNeedsVerification;
// true if STPShippingVC should be presented to collect or verify a shipping address
BOOL shouldPresentShippingAddress = (shippingAddressRequired && (shippingAddressIncomplete || verificationRequired));
// this handles a corner case where STPShippingVC should be presented because:
// - shipping address has been pre-filled
// - no verification is required, but the user still needs to enter a shipping method
BOOL shouldPresentShippingMethods = (shippingAddressRequired &&
!shippingAddressIncomplete &&
!verificationRequired &&
shippingMethodRequired);
return (shouldPresentShippingAddress || shouldPresentShippingMethods);
}
- (void)requestPayment {
WEAK(self);
[[[self.didAppearPromise voidFlatMap:^STPPromise * _Nonnull{
STRONG(self);
return self.loadingPromise;
}] onSuccess:^(__unused STPPaymentMethodTuple *tuple) {
STRONG(self);
if (!self) {
return;
}
if (self.state != STPPaymentContextStateNone) {
return;
}
if (!self.selectedPaymentMethod) {
[self presentPaymentMethodsViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self requestPaymentShouldPresentShippingViewController]) {
[self presentShippingViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self.selectedPaymentMethod isKindOfClass:[STPCard class]] ||
[self.selectedPaymentMethod isKindOfClass:[STPSource class]]) {
self.state = STPPaymentContextStateRequestingPayment;
STPPaymentResult *result = [[STPPaymentResult alloc] initWithSource:(id<STPSourceProtocol>)self.selectedPaymentMethod];
[self.delegate paymentContext:self didCreatePaymentResult:result completion:^(NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
if (error) {
[self didFinishWithStatus:STPPaymentStatusError error:error];
} else {
[self didFinishWithStatus:STPPaymentStatusSuccess error:nil];
}
});
}];
}
else if ([self.selectedPaymentMethod isKindOfClass:[STPApplePayPaymentMethod class]]) {
self.state = STPPaymentContextStateRequestingPayment;
PKPaymentRequest *paymentRequest = [self buildPaymentRequest];
STPShippingAddressSelectionBlock shippingAddressHandler = ^(STPAddress *shippingAddress, STPShippingAddressValidationBlock completion) {
// Apple Pay always returns a partial address here, so we won't
// update self.shippingAddress or self.shippingMethods
if ([self.delegate respondsToSelector:@selector(paymentContext:didUpdateShippingAddress:completion:)]) {
[self.delegate paymentContext:self didUpdateShippingAddress:shippingAddress completion:^(STPShippingStatus status, __unused NSError *shippingValidationError, NSArray<PKShippingMethod *> *shippingMethods, __unused PKShippingMethod *selectedMethod) {
completion(status, shippingMethods, self.paymentSummaryItems);
}];
}
else {
completion(STPShippingStatusValid, self.shippingMethods, self.paymentSummaryItems);
}
};
STPShippingMethodSelectionBlock shippingMethodHandler = ^(PKShippingMethod *shippingMethod, STPPaymentSummaryItemCompletionBlock completion) {
self.selectedShippingMethod = shippingMethod;
[self.delegate paymentContextDidChange:self];
completion(self.paymentSummaryItems);
};
STPPaymentAuthorizationBlock paymentHandler = ^(PKPayment *payment) {
self.selectedShippingMethod = payment.shippingMethod;
self.shippingAddress = [[STPAddress alloc] initWithPKContact:payment.shippingContact];
self.shippingAddressNeedsVerification = NO;
[self.delegate paymentContextDidChange:self];
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
STPCustomerContext *customerContext = (STPCustomerContext *)self.apiAdapter;
[customerContext updateCustomerWithShippingAddress:self.shippingAddress completion:nil];
}
};
STPApplePaySourceHandlerBlock applePaySourceHandler = ^(id<STPSourceProtocol> source, STPErrorBlock completion) {
[self.apiAdapter attachSourceToCustomer:source completion:^(NSError *attachSourceError) {
stpDispatchToMainThreadIfNecessary(^{
if (attachSourceError) {
completion(attachSourceError);
} else {
id<STPSourceProtocol> paymentResultSource = source;
/**
When createCardSources is false, the SDK:
1. Sends the token to customers/[id]/sources. This
adds token.card to the customer's sources list.
Surprisingly, attaching token.card to the customer
will fail.
2. Returns token.card to didCreatePaymentResult,
where the user tells their backend to create a charge.
A charge request with the token ID and customer ID
will fail because the token is not linked to the
customer (the card is).
*/
if ([source isKindOfClass:[STPToken class]]) {
paymentResultSource = ((STPToken *)source).card;
}
STPPaymentResult *result = [[STPPaymentResult alloc] initWithSource:paymentResultSource];
[self.delegate paymentContext:self didCreatePaymentResult:result completion:^(NSError * error) {
// for Apple Pay, the didFinishWithStatus callback is fired later when Apple Pay VC finishes
if (error) {
completion(error);
} else {
completion(nil);
}
}];
}
});
}];
};
PKPaymentAuthorizationViewController *paymentAuthVC;
paymentAuthVC = [PKPaymentAuthorizationViewController
stp_controllerWithPaymentRequest:paymentRequest
apiClient:self.apiClient
createSource:self.configuration.createCardSources
onShippingAddressSelection:shippingAddressHandler
onShippingMethodSelection:shippingMethodHandler
onPaymentAuthorization:paymentHandler
onTokenCreation:applePaySourceHandler
onFinish:^(STPPaymentStatus status, NSError * _Nullable error) {
[self.hostViewController dismissViewControllerAnimated:[self transitionAnimationsEnabled]
completion:^{
[self didFinishWithStatus:status
error:error];
}];
}];
[self.hostViewController presentViewController:paymentAuthVC
animated:[self transitionAnimationsEnabled]
completion:nil];
}
}] onFailure:^(NSError *error) {
STRONG(self);
[self didFinishWithStatus:STPPaymentStatusError error:error];
}];
}
- (void)didFinishWithStatus:(STPPaymentStatus)status
error:(nullable NSError *)error {
self.state = STPPaymentContextStateNone;
[self.delegate paymentContext:self
didFinishWithStatus:status
error:error];
}
- (PKPaymentRequest *)buildPaymentRequest {
if (!self.configuration.appleMerchantIdentifier || !self.paymentAmount) {
return nil;
}
PKPaymentRequest *paymentRequest = [Stripe paymentRequestWithMerchantIdentifier:self.configuration.appleMerchantIdentifier country:self.paymentCountry currency:self.paymentCurrency];
NSArray<PKPaymentSummaryItem *> *summaryItems = self.paymentSummaryItems;
paymentRequest.paymentSummaryItems = summaryItems;
paymentRequest.requiredBillingAddressFields = [STPAddress applePayAddressFieldsFromBillingAddressFields:self.configuration.requiredBillingAddressFields];
if (@available(iOS 11, *)) {
NSSet<PKContactField> *requiredFields = [STPAddress pkContactFieldsFromStripeContactFields:self.configuration.requiredShippingAddressFields];
if (requiredFields) {
paymentRequest.requiredShippingContactFields = requiredFields;
}
}
else {
paymentRequest.requiredShippingAddressFields = [STPAddress pkAddressFieldsFromStripeContactFields:self.configuration.requiredShippingAddressFields];
}
paymentRequest.currencyCode = self.paymentCurrency.uppercaseString;
if (self.selectedShippingMethod != nil) {
NSMutableArray<PKShippingMethod *>* orderedShippingMethods = [self.shippingMethods mutableCopy];
[orderedShippingMethods removeObject:self.selectedShippingMethod];
[orderedShippingMethods insertObject:self.selectedShippingMethod atIndex:0];
paymentRequest.shippingMethods = orderedShippingMethods;
}
else {
paymentRequest.shippingMethods = self.shippingMethods;
}
paymentRequest.shippingType = [[self class] pkShippingType:self.configuration.shippingType];;
if (self.shippingAddress != nil) {
paymentRequest.shippingContact = [self.shippingAddress PKContactValue];
}
return paymentRequest;
}
+ (PKShippingType)pkShippingType:(STPShippingType)shippingType {
switch (shippingType) {
case STPShippingTypeShipping:
return PKShippingTypeShipping;
case STPShippingTypeDelivery:
return PKShippingTypeDelivery;
}
}
static char kSTPPaymentCoordinatorAssociatedObjectKey;
- (void)artificiallyRetain:(NSObject *)host {
objc_setAssociatedObject(host, &kSTPPaymentCoordinatorAssociatedObjectKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end