From f7241676b396276db1c31dbd455d1052f0079d63 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Thu, 8 Aug 2019 15:55:36 -0700 Subject: [PATCH 1/6] STPPaymentIntentLastError --- Stripe.xcodeproj/project.pbxproj | 18 ++- Stripe/Payments/STPPaymentHandler.m | 27 +++- Stripe/PublicHeaders/STPPaymentHandler.h | 7 ++ Stripe/PublicHeaders/STPPaymentIntent.h | 7 +- .../STPPaymentIntentLastPaymentError.h | 117 ++++++++++++++++++ Stripe/PublicHeaders/Stripe.h | 1 + Stripe/STPPaymentIntent.m | 4 + Stripe/STPPaymentIntentLastPaymentError.m | 82 ++++++++++++ Tests/Tests/PaymentIntent.json | 32 +++++ Tests/Tests/STPPaymentIntentLastErrorTest.m | 46 +++++++ Tests/Tests/STPPaymentIntentTest.m | 9 ++ 11 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h create mode 100644 Stripe/STPPaymentIntentLastPaymentError.m create mode 100644 Tests/Tests/STPPaymentIntentLastErrorTest.m diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj index 97098c14c18..7cc70ccb55e 100644 --- a/Stripe.xcodeproj/project.pbxproj +++ b/Stripe.xcodeproj/project.pbxproj @@ -662,6 +662,11 @@ B69FEC40222EE8FE00273A16 /* STPPaymentMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = B69FEC3C222EE8FE00273A16 /* STPPaymentMethod.m */; }; B69FEC42222EE9E000273A16 /* STPPaymentMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = B69FEC41222EE9E000273A16 /* STPPaymentMethod.h */; settings = {ATTRIBUTES = (Public, ); }; }; B69FEC43222EE9E000273A16 /* STPPaymentMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = B69FEC41222EE9E000273A16 /* STPPaymentMethod.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B6A46F7622FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h in Headers */ = {isa = PBXBuildFile; fileRef = B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B6A46F7722FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h in Headers */ = {isa = PBXBuildFile; fileRef = B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B6A46F7822FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */; }; + B6A46F7922FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */; }; + B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m */; }; B6B41F71223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B41F70223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m */; }; B6B41F73223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B41F72223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m */; }; B6B41F75223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = B6B41F74223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h */; }; @@ -1608,6 +1613,9 @@ B69CFB4922370547001E9885 /* STPPaymentMethodCardChecks+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPPaymentMethodCardChecks+Private.h"; sourceTree = ""; }; B69FEC3C222EE8FE00273A16 /* STPPaymentMethod.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethod.m; sourceTree = ""; }; B69FEC41222EE9E000273A16 /* STPPaymentMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STPPaymentMethod.h; path = PublicHeaders/STPPaymentMethod.h; sourceTree = ""; }; + B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = STPPaymentIntentLastPaymentError.h; path = PublicHeaders/STPPaymentIntentLastPaymentError.h; sourceTree = ""; }; + B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastPaymentError.m; sourceTree = ""; }; + B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastErrorTest.m; sourceTree = ""; }; B6B41F70223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodCardWalletMasterpassTest.m; sourceTree = ""; }; B6B41F72223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodCardWalletVisaCheckoutTest.m; sourceTree = ""; }; B6B41F74223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPPaymentMethodCardWallet+Private.h"; sourceTree = ""; }; @@ -2304,6 +2312,7 @@ C18867D81E8B093300A77634 /* Unit */ = { isa = PBXGroup; children = ( + B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m */, 04A4C3911C4F263300B3B290 /* NSArray+StripeTest.m */, C11810981CC6D46D0022FB55 /* NSDecimalNumber+StripeTest.m */, 8BB97F071F26645B0095122A /* NSDictionary+StripeTest.m */, @@ -2743,6 +2752,8 @@ B6F1608E223350640088C970 /* STPPaymentIntentAction.h */, B6F16094223351C20088C970 /* STPPaymentIntentActionRedirectToURL.h */, B3BDCAC720EEF22D0034F7F5 /* STPPaymentIntentEnums.h */, + B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */, + B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */, B3BDCAD520EEF5EC0034F7F5 /* STPPaymentIntentParams.h */, B3BDCAD220EEF5E00034F7F5 /* STPPaymentIntentParams.m */, B36C6D6B2193671400D17575 /* STPPaymentIntentSourceAction.h */, @@ -2778,9 +2789,9 @@ B6B5FC40222F4C0200440249 /* STPPaymentMethodThreeDSecureUsage.m */, B613DD3022C536C900C7603F /* STPSetupIntent.h */, B613DD3122C536C900C7603F /* STPSetupIntent.m */, - B613DD3522C5452500C7603F /* STPSetupIntentEnums.h */, B640DB1022C58E82003C8810 /* STPSetupIntentConfirmParams.h */, B640DB1122C58E82003C8810 /* STPSetupIntentConfirmParams.m */, + B613DD3522C5452500C7603F /* STPSetupIntentEnums.h */, C1D7B51E1E36C32F002181F5 /* STPSource.h */, C1D7B51F1E36C32F002181F5 /* STPSource.m */, F19491DD1E5F6B8C001E1FC2 /* STPSourceCardDetails.h */, @@ -2951,6 +2962,7 @@ B613DD3422C5370400C7603F /* STPSetupIntent.h in Headers */, 319A609522E9186B00AACF66 /* STDSProtocolErrorEvent.h in Headers */, 049E84D91A605EF0000B66CD /* Stripe.h in Headers */, + B6A46F7722FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h in Headers */, 0439B9881C454F97005A1ED5 /* STPPaymentOptionsViewController.h in Headers */, 319A609D22E9186B00AACF66 /* STDSTransaction.h in Headers */, 3604007B22C18DAE004CF80B /* STPThreeDSTextFieldCustomization.h in Headers */, @@ -3280,6 +3292,7 @@ C15608DD1FE08F2E0032AE66 /* UIView+Stripe_SafeAreaBounds.h in Headers */, 3621DDCA22A5E4FD00281BC4 /* STPAuthenticationContext.h in Headers */, F1852F931D80B6EC00367C86 /* STPStringUtils.h in Headers */, + B6A46F7622FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h in Headers */, 049A3FAE1CC9AA9900F57DE7 /* STPAddressViewModel.h in Headers */, B3A99BC31FEAF2CA003F6ED3 /* STPLegalEntityParams.h in Headers */, C175B7941FE834A3009F5A0E /* STPCustomer+Private.h in Headers */, @@ -3932,6 +3945,7 @@ C1080F4C1CBED48A007B2D89 /* STPAddressTests.m in Sources */, C1C02CCE1ECCE92900DF5643 /* STPEphemeralKeyTest.m in Sources */, 36D4EA6122DD33DF00619BA8 /* STPSetupIntentConfirmParamsTest.m in Sources */, + B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m in Sources */, B36C6D782193A16F00D17575 /* STPIntentActionTest.m in Sources */, C14C4DB11EC3B34500C2FDF6 /* STPAPIRequestTest.m in Sources */, F1D96F9B1DC7DCDE00477E64 /* STPLocalizationUtils+STPTestAdditions.m in Sources */, @@ -3972,6 +3986,7 @@ B69FEC40222EE8FE00273A16 /* STPPaymentMethod.m in Sources */, F1DEB88D1E2047CA0066B8E8 /* STPCoreTableViewController.m in Sources */, F1C7B8D41DBECF2400D9F6F0 /* STPDispatchFunctions.m in Sources */, + B6A46F7922FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */, B382D6611FE8BEA0009B56AB /* STPValidatedTextField.m in Sources */, 04633B141CD45215009D4FB5 /* PKPayment+Stripe.m in Sources */, 04633AFF1CD129C0009D4FB5 /* NSString+Stripe.m in Sources */, @@ -4182,6 +4197,7 @@ 04695ADC1C77F9EF00E08063 /* STPPhoneNumberValidator.m in Sources */, C1785F5E1EC60B5E00E9CFAC /* STPCardIOProxy.m in Sources */, 04CDB5141A5F30A700B854EE /* STPToken.m in Sources */, + B6A46F7822FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */, C118108A1CC6B00D0022FB55 /* STPApplePayPaymentOption.m in Sources */, 04A488341CA34D3000506E53 /* STPEmailAddressValidator.m in Sources */, 0433EB4C1BD06313003912B4 /* NSDictionary+Stripe.m in Sources */, diff --git a/Stripe/Payments/STPPaymentHandler.m b/Stripe/Payments/STPPaymentHandler.m index 6c0915d6e03..0708156dcff 100644 --- a/Stripe/Payments/STPPaymentHandler.m +++ b/Stripe/Payments/STPPaymentHandler.m @@ -17,6 +17,7 @@ #import "STPAPIClient+Private.h" #import "STPAuthenticationContext.h" #import "STPPaymentIntent.h" +#import "STPPaymentIntentLastPaymentError.h" #import "STPPaymentIntentParams.h" #import "STPPaymentHandlerActionParams.h" #import "STPIntentAction+Private.h" @@ -375,11 +376,25 @@ - (BOOL)_handlePaymentIntentStatusForAction:(STPPaymentHandlerPaymentIntentActio [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerIntentStatusErrorCode userInfo:@{@"STPPaymentIntent": paymentIntent.description}]]; break; - case STPPaymentIntentStatusRequiresPaymentMethod: + case STPPaymentIntentStatusRequiresPaymentMethod: { // If the user forgot to attach a PaymentMethod, they get an error before this point. - // If authentication fails, or the card is declined, the PaymentIntent transitions to this state. - [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; + // If confirmation fails (eg not authenticated, card declined) the PaymentIntent transitions to this state. + switch (paymentIntent.lastPaymentError.type) { + case STPPaymentIntentLastPaymentErrorTypeAuthentication: + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; + break; + case STPPaymentIntentLastPaymentErrorTypeCard: + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:@{NSLocalizedDescriptionKey: paymentIntent.lastPaymentError.message}]]; + break; + default: + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:nil]]; + break; + } break; + } case STPPaymentIntentStatusRequiresConfirmation: [action completeWithStatus:STPPaymentHandlerActionStatusSucceeded error:nil]; break; @@ -801,6 +816,12 @@ - (NSError *)_errorForCode:(STPPaymentHandlerErrorCode)errorCode userInfo:(nulla userInfo[STPErrorMessageKey] = userInfo[STPErrorMessageKey] ?: @"There was an error in the Stripe3DS2 SDK."; userInfo[NSLocalizedDescriptionKey] = [NSError stp_unexpectedErrorMessage]; break; + + // Confirmation errors (eg card was declined) + case STPPaymentHandlerPaymentErrorCode: + userInfo[STPErrorMessageKey] = userInfo[STPErrorMessageKey] ?: @"There was an error confirming the Intent. Inspect the `paymentIntent.lastPaymentError` or `setupIntent.lastSetupError` property."; + userInfo[NSLocalizedDescriptionKey] = userInfo[NSLocalizedDescriptionKey] ?: [NSError stp_unexpectedErrorMessage]; + break; } return [NSError errorWithDomain:STPPaymentHandlerErrorDomain code:errorCode diff --git a/Stripe/PublicHeaders/STPPaymentHandler.h b/Stripe/PublicHeaders/STPPaymentHandler.h index f052cec846a..47f7ab516bf 100644 --- a/Stripe/PublicHeaders/STPPaymentHandler.h +++ b/Stripe/PublicHeaders/STPPaymentHandler.h @@ -87,6 +87,13 @@ typedef NS_ENUM(NSInteger, STPPaymentHandlerErrorCode) { If you're using Apple Pay, you must implement `STPAuthenticationContext prepareAuthenticationContextForPresentation:` */ STPPaymentHandlerRequiresAuthenticationContextErrorCode, + + /** + There was an error confirming the Intent. + + Inspect the `paymentIntent.lastPaymentError` or `setupIntent.lastSetupError` property. + */ + STPPaymentHandlerPaymentErrorCode, }; diff --git a/Stripe/PublicHeaders/STPPaymentIntent.h b/Stripe/PublicHeaders/STPPaymentIntent.h index 2ce83c549c8..0e2462f4fd3 100644 --- a/Stripe/PublicHeaders/STPPaymentIntent.h +++ b/Stripe/PublicHeaders/STPPaymentIntent.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN -@class STPIntentAction; +@class STPIntentAction, STPPaymentIntentLastPaymentError; /** A PaymentIntent tracks the process of collecting a payment from your customer. @@ -118,6 +118,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly) STPPaymentIntentSetupFutureUsage setupFutureUsage; +/** + The payment error encountered in the previous PaymentIntent confirmation. + */ +@property (nonatomic, nullable, readonly) STPPaymentIntentLastPaymentError *lastPaymentError; + #pragma mark - Deprecated /** diff --git a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h new file mode 100644 index 00000000000..d07b2fde7db --- /dev/null +++ b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h @@ -0,0 +1,117 @@ +// +// STPPaymentIntentLastError.h +// Stripe +// +// Created by Yuki Tokuhiro on 8/8/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +#import + +#import "STPAPIResponseDecodable.h" + +@class STPPaymentMethod; + +/** + The type of the error represented by `STPPaymentIntentLastError`. + + Some STPPaymentIntentLastError properties are only populated for certain error types. + */ +typedef NS_ENUM(NSUInteger, STPPaymentIntentLastPaymentErrorType) { + /** + An unknown error type. + */ + STPPaymentIntentLastPaymentErrorTypeUnknown, + + /** + An error connecting to Stripe's API. + */ + STPPaymentIntentLastPaymentErrorTypeAPIConnection, + + /** + An error with the Stripe API. + */ + STPPaymentIntentLastPaymentErrorTypeAPI, + + /** + A failure to authenticate your customer. + */ + STPPaymentIntentLastPaymentErrorTypeAuthentication, + + /* + Card errors are the most common type of error you should expect to handle. + They result when the user enters a card that can't be charged for some reason. + + Check the `declineCode` property for the decline code. The `message` property contains a message you can show to your users. + */ + STPPaymentIntentLastPaymentErrorTypeCard, + + /** + Keys for idempotent requests can only be used with the same parameters they were first used with. + */ + STPPaymentIntentLastPaymentErrorTypeIdempotency, + + /** + Invalid request errors. Typically, this is because your request has invalid parameters. + */ + STPPaymentIntentLastPaymentErrorTypeInvalidRequest, + + /** + Too many requests hit the API too quickly. + */ + STPPaymentIntentLastPaymentErrorTypeRateLimit, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + The payment error encountered in the previous PaymentIntent confirmation. + */ +@interface STPPaymentIntentLastPaymentError : NSObject + +/** + For some errors that could be handled programmatically, a short string indicating the error code reported. + + @see https://stripe.com/docs/error-codes + */ +@property (nonatomic, nullable, readonly) NSString *code; + +/** + For card errors resulting from a card issuer decline, a short string indicating the card issuer’s reason for the decline if they provide one. + + @see https://stripe.com/docs/declines#issuer-declines + */ +@property (nonatomic, nullable, readonly) NSString *declineCode; + +/** + A URL to more information about the error code reported. + + @see https://stripe.com/docs/error-codes + */ +@property (nonatomic, readonly) NSString *docURL; + +/** + A human-readable message providing more details about the error. + For card errors, these messages can be shown to your users. + */ +@property (nonatomic, readonly) NSString *message; + +/** + If the error is parameter-specific, the parameter related to the error. + For example, you can use this to display a message near the correct form field. + */ +@property (nonatomic, nullable, readonly) NSString *param; + +/** + The PaymentMethod object for errors returned on a request involving a PaymentMethod. + */ +@property (nonatomic, nullable, readonly) STPPaymentMethod *paymentMethod; + +/** + The type of error returned. + */ +@property (nonatomic, readonly) STPPaymentIntentLastPaymentErrorType type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe/PublicHeaders/Stripe.h b/Stripe/PublicHeaders/Stripe.h index a641f1eaccb..e2f63b08746 100644 --- a/Stripe/PublicHeaders/Stripe.h +++ b/Stripe/PublicHeaders/Stripe.h @@ -48,6 +48,7 @@ #import "STPPaymentIntentAction.h" #import "STPPaymentIntentActionRedirectToURL.h" #import "STPPaymentIntentEnums.h" +#import "STPPaymentIntentLastPaymentError.h" #import "STPPaymentIntentParams.h" #import "STPPaymentIntentSourceAction.h" #import "STPPaymentIntentSourceActionAuthorizeWithURL.h" diff --git a/Stripe/STPPaymentIntent.m b/Stripe/STPPaymentIntent.m index d0ccc6b5b17..40e7bcd645c 100644 --- a/Stripe/STPPaymentIntent.m +++ b/Stripe/STPPaymentIntent.m @@ -10,6 +10,7 @@ #import "STPPaymentIntent+Private.h" #import "STPPaymentIntentSourceAction.h" #import "STPPaymentIntentAction.h" +#import "STPPaymentIntentLastPaymentError.h" #import "STPPaymentMethod+Private.h" #import "NSDictionary+Stripe.h" @@ -33,6 +34,7 @@ @interface STPPaymentIntent () @property (nonatomic, assign, readwrite) STPPaymentIntentStatus status; @property (nonatomic, copy, nullable, readwrite) NSArray *paymentMethodTypes; @property (nonatomic) STPPaymentIntentSetupFutureUsage setupFutureUsage; +@property (nonatomic, nullable, readwrite) STPPaymentIntentLastPaymentError *lastPaymentError; @property (nonatomic, copy, nonnull, readwrite) NSDictionary *allResponseFields; @end @@ -56,6 +58,7 @@ - (NSString *)description { [NSString stringWithFormat:@"created = %@", self.created], [NSString stringWithFormat:@"currency = %@", self.currency], [NSString stringWithFormat:@"description = %@", self.stripeDescription], + [NSString stringWithFormat:@"lastPaymentError = %@", self.lastPaymentError], [NSString stringWithFormat:@"livemode = %@", self.livemode ? @"YES" : @"NO"], [NSString stringWithFormat:@"nextAction = %@", self.nextAction], [NSString stringWithFormat:@"paymentMethodId = %@", self.paymentMethodId], @@ -188,6 +191,7 @@ + (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)r paymentIntent.status = [[self class] statusFromString:rawStatus]; NSString *rawSetupFutureUsage = [dict stp_stringForKey:@"setup_future_usage"]; paymentIntent.setupFutureUsage = rawSetupFutureUsage ? [[self class] setupFutureUsageFromString:rawSetupFutureUsage] : STPPaymentIntentSetupFutureUsageNone; + paymentIntent.lastPaymentError = [STPPaymentIntentLastPaymentError decodedObjectFromAPIResponse:[dict stp_dictionaryForKey:@"last_payment_error"]]; paymentIntent.allResponseFields = dict; diff --git a/Stripe/STPPaymentIntentLastPaymentError.m b/Stripe/STPPaymentIntentLastPaymentError.m new file mode 100644 index 00000000000..3dd2ac65274 --- /dev/null +++ b/Stripe/STPPaymentIntentLastPaymentError.m @@ -0,0 +1,82 @@ +// +// STPPaymentIntentLastError.m +// Stripe +// +// Created by Yuki Tokuhiro on 8/8/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentIntentLastPaymentError.h" + +#import "NSDictionary+Stripe.h" +#import "STPPaymentMethod.h" + +@interface STPPaymentIntentLastPaymentError() +@property (nonatomic, copy) NSString *code; +@property (nonatomic, copy) NSString *declineCode; +@property (nonatomic, copy) NSString *docURL; +@property (nonatomic, copy) NSString *message; +@property (nonatomic, copy) NSString *param; +@property (nonatomic) STPPaymentMethod *paymentMethod; +@property (nonatomic) STPPaymentIntentLastPaymentErrorType type; +@property (nonatomic, copy, nonnull, readwrite) NSDictionary *allResponseFields; +@end + +@implementation STPPaymentIntentLastPaymentError + +- (NSString *)description { + NSArray *props = @[ + // Object + [NSString stringWithFormat:@"%@: %p", NSStringFromClass([self class]), self], + + // PaymentIntentLastError details (alphabetical) + [NSString stringWithFormat:@"code = %@", self.code], + [NSString stringWithFormat:@"declineCode = %@", self.declineCode], + [NSString stringWithFormat:@"docURL = %@", self.docURL], + [NSString stringWithFormat:@"message = %@", self.message], + [NSString stringWithFormat:@"param = %@", self.param], + [NSString stringWithFormat:@"paymentMethod = %@", self.paymentMethod], + [NSString stringWithFormat:@"type = %@", self.allResponseFields[@"type"]], + ]; + + return [NSString stringWithFormat:@"<%@>", [props componentsJoinedByString:@"; "]]; +} + ++ (STPPaymentIntentLastPaymentErrorType)typeFromString:(NSString *)string { + NSDictionary *map = @{ + @"api_connection_error": @(STPPaymentIntentLastPaymentErrorTypeAPIConnection), + @"api_error": @(STPPaymentIntentLastPaymentErrorTypeAPI), + @"authentication_error": @(STPPaymentIntentLastPaymentErrorTypeAuthentication), + @"card_error": @(STPPaymentIntentLastPaymentErrorTypeCard), + @"idempotency_error": @(STPPaymentIntentLastPaymentErrorTypeIdempotency), + @"invalid_request_error": @(STPPaymentIntentLastPaymentErrorTypeInvalidRequest), + @"rate_limit_error": @(STPPaymentIntentLastPaymentErrorTypeRateLimit), + }; + + NSString *key = string.lowercaseString; + NSNumber *statusNumber = map[key] ?: @(STPPaymentIntentLastPaymentErrorTypeUnknown); + return statusNumber.integerValue; +} + +#pragma mark - STPAPIResponseDecodable + ++ (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNulls]; + if (!dict) { + return nil; + } + + STPPaymentIntentLastPaymentError *lastError = [self new]; + lastError.code = [dict stp_stringForKey:@"code"]; + lastError.declineCode = [dict stp_stringForKey:@"declineCode"]; + lastError.docURL = [dict stp_stringForKey:@"doc_url"]; + lastError.message = [dict stp_stringForKey:@"message"]; + lastError.param = [dict stp_stringForKey:@"param"]; + lastError.paymentMethod = [STPPaymentMethod decodedObjectFromAPIResponse:[dict stp_dictionaryForKey:@"payment_method"]]; + lastError.type = [self typeFromString:[dict stp_stringForKey:@"type"]]; + lastError.allResponseFields = dict; + + return lastError; +} + +@end diff --git a/Tests/Tests/PaymentIntent.json b/Tests/Tests/PaymentIntent.json index 0d37c9a11b0..efb97de0f46 100644 --- a/Tests/Tests/PaymentIntent.json +++ b/Tests/Tests/PaymentIntent.json @@ -12,6 +12,38 @@ "created": 1530911040, "currency": "usd", "description": "My Sample PaymentIntent", + "last_payment_error": { + "code": "payment_intent_authentication_failure", + "doc_url": "https://stripe.com/docs/error-codes/payment-intent-authentication-failure", + "message": "The provided PaymentMethod has failed authentication. You can provide payment_method_data or a new PaymentMethod to attempt to fulfill this PaymentIntent again.", + "payment_method": { + "id": "pm_1F5KZ4KlwPmebFhph828lKsZ", + "object": "payment_method", + "billing_details": { + "address": { + }, + }, + "card": { + "brand": "visa", + "checks": { + }, + "country": "US", + "exp_month": 2, + "exp_year": 2042, + "funding": "credit", + "last4": "3063", + "three_d_secure_usage": { + "supported": true + }, + }, + "created": 1565305114, + "livemode": false, + "metadata": { + }, + "type": "card" + }, + "type": "invalid_request_error" + }, "livemode": false, "next_source_action": { "type": "authorize_with_url", diff --git a/Tests/Tests/STPPaymentIntentLastErrorTest.m b/Tests/Tests/STPPaymentIntentLastErrorTest.m new file mode 100644 index 00000000000..b3eef1dffe2 --- /dev/null +++ b/Tests/Tests/STPPaymentIntentLastErrorTest.m @@ -0,0 +1,46 @@ +// +// STPPaymentIntentLastErrorTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 8/8/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +#import + +#import +#import "STPTestUtils.h" +#import "STPFixtures.h" + +@interface STPPaymentIntentLastPaymentError (Testing) ++ (STPPaymentIntentLastPaymentErrorType)typeFromString:(NSString *)string; +@end + +@interface STPPaymentIntentLastErrorTest : XCTestCase + +@end + +@implementation STPPaymentIntentLastErrorTest + +- (void)testTypeFromString { + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"api_connection_error"], STPPaymentIntentLastPaymentErrorTypeAPIConnection); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"API_CONNECTION_ERROR"], STPPaymentIntentLastPaymentErrorTypeAPIConnection); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"api_error"], STPPaymentIntentLastPaymentErrorTypeAPI); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"API_ERROR"], STPPaymentIntentLastPaymentErrorTypeAPI); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"authentication_error"], STPPaymentIntentLastPaymentErrorTypeAuthentication); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"AUTHENTICATION_ERROR"], STPPaymentIntentLastPaymentErrorTypeAuthentication); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"card_error"], STPPaymentIntentLastPaymentErrorTypeCard); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"CARD_ERROR"], STPPaymentIntentLastPaymentErrorTypeCard); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"idempotency_error"], STPPaymentIntentLastPaymentErrorTypeIdempotency); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"IDEMPOTENCY_ERROR"], STPPaymentIntentLastPaymentErrorTypeIdempotency); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"invalid_request_error"], STPPaymentIntentLastPaymentErrorTypeInvalidRequest); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"INVALID_REQUEST_ERROR"], STPPaymentIntentLastPaymentErrorTypeInvalidRequest); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"rate_limit_error"], STPPaymentIntentLastPaymentErrorTypeRateLimit); + XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"RATE_LIMIT_ERROR"], STPPaymentIntentLastPaymentErrorTypeRateLimit); +} + +#pragma mark - STPAPIResponseDecodable Tests + +// STPPaymentIntentLastError is a sub-object of STPPaymentIntent, see STPPaymentIntentTest + +@end diff --git a/Tests/Tests/STPPaymentIntentTest.m b/Tests/Tests/STPPaymentIntentTest.m index 2d821237881..eb76659867d 100644 --- a/Tests/Tests/STPPaymentIntentTest.m +++ b/Tests/Tests/STPPaymentIntentTest.m @@ -213,6 +213,15 @@ - (void)testDecodedObjectFromAPIResponseMapping { XCTAssertEqual(paymentIntent.setupFutureUsage, STPPaymentIntentSetupFutureUsageNone); XCTAssertEqualObjects(paymentIntent.paymentMethodTypes, @[@(STPPaymentMethodTypeCard)]); + + // lastPaymentError + + XCTAssertNotNil(paymentIntent.lastPaymentError); + XCTAssertEqualObjects(paymentIntent.lastPaymentError.code, @"payment_intent_authentication_failure"); + XCTAssertEqualObjects(paymentIntent.lastPaymentError.docURL, @"https://stripe.com/docs/error-codes/payment-intent-authentication-failure"); + XCTAssertEqualObjects(paymentIntent.lastPaymentError.message, @"The provided PaymentMethod has failed authentication. You can provide payment_method_data or a new PaymentMethod to attempt to fulfill this PaymentIntent again."); + XCTAssertNotNil(paymentIntent.lastPaymentError.paymentMethod); + XCTAssertEqual(paymentIntent.lastPaymentError.type, STPPaymentIntentLastErrorTypeInvalidRequest); XCTAssertNotEqual(paymentIntent.allResponseFields, response, @"should have own copy of fields"); XCTAssertEqualObjects(paymentIntent.allResponseFields, response, @"fields values should match"); From fc4b16b210cbd087284ec358e1a74df1063c55f2 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Fri, 9 Aug 2019 14:24:49 -0700 Subject: [PATCH 2/6] STPSetupIntentLastSetupError --- Stripe.xcodeproj/project.pbxproj | 24 +++- Stripe/Payments/STPPaymentHandler.m | 41 +++--- .../STPPaymentIntentLastPaymentError.h | 8 +- Stripe/PublicHeaders/STPSetupIntent.h | 7 +- .../STPSetupIntentLastSetupError.h | 119 ++++++++++++++++++ Stripe/PublicHeaders/Stripe.h | 1 + Stripe/STPPaymentIntentLastPaymentError.m | 2 +- Stripe/STPSetupIntent.m | 4 + Stripe/STPSetupIntentLastSetupError.m | 81 ++++++++++++ ...=> STPPaymentIntentLastpaymentErrorTest.m} | 6 +- Tests/Tests/STPPaymentIntentTest.m | 2 +- .../Tests/STPSetupIntentLastSetupErrorTest.m | 47 +++++++ Tests/Tests/STPSetupIntentTest.m | 9 ++ Tests/Tests/SetupIntent.json | 48 ++++++- 14 files changed, 367 insertions(+), 32 deletions(-) create mode 100644 Stripe/PublicHeaders/STPSetupIntentLastSetupError.h create mode 100644 Stripe/STPSetupIntentLastSetupError.m rename Tests/Tests/{STPPaymentIntentLastErrorTest.m => STPPaymentIntentLastpaymentErrorTest.m} (94%) create mode 100644 Tests/Tests/STPSetupIntentLastSetupErrorTest.m diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj index 7cc70ccb55e..6702d2f9b98 100644 --- a/Stripe.xcodeproj/project.pbxproj +++ b/Stripe.xcodeproj/project.pbxproj @@ -610,6 +610,11 @@ B640DB1722C69A8E003C8810 /* STPSetupIntent+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = B640DB1622C69A8E003C8810 /* STPSetupIntent+Private.h */; }; B640DB1822C69A9D003C8810 /* STPSetupIntent+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = B640DB1622C69A8E003C8810 /* STPSetupIntent+Private.h */; }; B640DB1A22C69C01003C8810 /* STPSetupIntentFunctionalTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B640DB1922C69C01003C8810 /* STPSetupIntentFunctionalTest.m */; }; + B64763B422FE193800C01BC0 /* STPSetupIntentLastSetupError.h in Headers */ = {isa = PBXBuildFile; fileRef = B64763B222FE193800C01BC0 /* STPSetupIntentLastSetupError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B64763B522FE193800C01BC0 /* STPSetupIntentLastSetupError.h in Headers */ = {isa = PBXBuildFile; fileRef = B64763B222FE193800C01BC0 /* STPSetupIntentLastSetupError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B64763B622FE193800C01BC0 /* STPSetupIntentLastSetupError.m in Sources */ = {isa = PBXBuildFile; fileRef = B64763B322FE193800C01BC0 /* STPSetupIntentLastSetupError.m */; }; + B64763B722FE193900C01BC0 /* STPSetupIntentLastSetupError.m in Sources */ = {isa = PBXBuildFile; fileRef = B64763B322FE193800C01BC0 /* STPSetupIntentLastSetupError.m */; }; + B64763B922FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B64763B822FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m */; }; B664D64922B800AF00E6354B /* STPThreeDSButtonCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = B664D64722B800AF00E6354B /* STPThreeDSButtonCustomization.m */; }; B664D64B22B8034D00E6354B /* STPThreeDSCustomization+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = B664D64A22B8034D00E6354B /* STPThreeDSCustomization+Private.h */; }; B664D64F22B8085900E6354B /* STPThreeDSUICustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = B664D64D22B8085900E6354B /* STPThreeDSUICustomization.m */; }; @@ -666,7 +671,7 @@ B6A46F7722FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h in Headers */ = {isa = PBXBuildFile; fileRef = B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */; settings = {ATTRIBUTES = (Public, ); }; }; B6A46F7822FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */; }; B6A46F7922FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */; }; - B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m */; }; + B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m */; }; B6B41F71223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B41F70223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m */; }; B6B41F73223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B41F72223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m */; }; B6B41F75223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = B6B41F74223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h */; }; @@ -1577,6 +1582,9 @@ B640DB1122C58E82003C8810 /* STPSetupIntentConfirmParams.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPSetupIntentConfirmParams.m; sourceTree = ""; }; B640DB1622C69A8E003C8810 /* STPSetupIntent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPSetupIntent+Private.h"; sourceTree = ""; }; B640DB1922C69C01003C8810 /* STPSetupIntentFunctionalTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPSetupIntentFunctionalTest.m; sourceTree = ""; }; + B64763B222FE193800C01BC0 /* STPSetupIntentLastSetupError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = STPSetupIntentLastSetupError.h; path = PublicHeaders/STPSetupIntentLastSetupError.h; sourceTree = ""; }; + B64763B322FE193800C01BC0 /* STPSetupIntentLastSetupError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPSetupIntentLastSetupError.m; sourceTree = ""; }; + B64763B822FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPSetupIntentLastSetupErrorTest.m; sourceTree = ""; }; B664D64722B800AF00E6354B /* STPThreeDSButtonCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPThreeDSButtonCustomization.m; sourceTree = ""; }; B664D64A22B8034D00E6354B /* STPThreeDSCustomization+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPThreeDSCustomization+Private.h"; sourceTree = ""; }; B664D64D22B8085900E6354B /* STPThreeDSUICustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPThreeDSUICustomization.m; sourceTree = ""; }; @@ -1615,7 +1623,7 @@ B69FEC41222EE9E000273A16 /* STPPaymentMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STPPaymentMethod.h; path = PublicHeaders/STPPaymentMethod.h; sourceTree = ""; }; B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = STPPaymentIntentLastPaymentError.h; path = PublicHeaders/STPPaymentIntentLastPaymentError.h; sourceTree = ""; }; B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastPaymentError.m; sourceTree = ""; }; - B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastErrorTest.m; sourceTree = ""; }; + B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastpaymentErrorTest.m; sourceTree = ""; }; B6B41F70223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodCardWalletMasterpassTest.m; sourceTree = ""; }; B6B41F72223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodCardWalletVisaCheckoutTest.m; sourceTree = ""; }; B6B41F74223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPPaymentMethodCardWallet+Private.h"; sourceTree = ""; }; @@ -2312,7 +2320,8 @@ C18867D81E8B093300A77634 /* Unit */ = { isa = PBXGroup; children = ( - B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m */, + B64763B822FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m */, + B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m */, 04A4C3911C4F263300B3B290 /* NSArray+StripeTest.m */, C11810981CC6D46D0022FB55 /* NSDecimalNumber+StripeTest.m */, 8BB97F071F26645B0095122A /* NSDictionary+StripeTest.m */, @@ -2792,6 +2801,8 @@ B640DB1022C58E82003C8810 /* STPSetupIntentConfirmParams.h */, B640DB1122C58E82003C8810 /* STPSetupIntentConfirmParams.m */, B613DD3522C5452500C7603F /* STPSetupIntentEnums.h */, + B64763B222FE193800C01BC0 /* STPSetupIntentLastSetupError.h */, + B64763B322FE193800C01BC0 /* STPSetupIntentLastSetupError.m */, C1D7B51E1E36C32F002181F5 /* STPSource.h */, C1D7B51F1E36C32F002181F5 /* STPSource.m */, F19491DD1E5F6B8C001E1FC2 /* STPSourceCardDetails.h */, @@ -3073,6 +3084,7 @@ F1FA6F931E258F6800EB444D /* STPCoreViewController+Private.h in Headers */, 04793F571D1D9C0200B3C551 /* STPSourceProtocol.h in Headers */, 04A4883E1CA3568800506E53 /* STPBlocks.h in Headers */, + B64763B522FE193800C01BC0 /* STPSetupIntentLastSetupError.h in Headers */, 045D71211CEFA57000F6CD65 /* UIViewController+Stripe_Promises.h in Headers */, C159932A1D88084D0047950D /* STPShippingAddressViewController.h in Headers */, B621F054223454E9002141B7 /* STPPaymentMethodCardWallet.h in Headers */, @@ -3300,6 +3312,7 @@ 0426B96E1CEADC98006AC8DD /* STPColorUtils.h in Headers */, B61C996522BBFA12004980FD /* STPAppInfo.h in Headers */, 04B31DDA1D09A4DC00EF1631 /* STPPaymentConfiguration+Private.h in Headers */, + B64763B422FE193800C01BC0 /* STPSetupIntentLastSetupError.h in Headers */, F1D96F961DC7D82400477E64 /* STPLocalizationUtils.h in Headers */, B6DB0CA6223817A300AEF640 /* STPPaymentMethodEnums.h in Headers */, 3604006F22C18C78004CF80B /* STPThreeDSFooterCustomization.h in Headers */, @@ -3945,13 +3958,14 @@ C1080F4C1CBED48A007B2D89 /* STPAddressTests.m in Sources */, C1C02CCE1ECCE92900DF5643 /* STPEphemeralKeyTest.m in Sources */, 36D4EA6122DD33DF00619BA8 /* STPSetupIntentConfirmParamsTest.m in Sources */, - B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastErrorTest.m in Sources */, + B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m in Sources */, B36C6D782193A16F00D17575 /* STPIntentActionTest.m in Sources */, C14C4DB11EC3B34500C2FDF6 /* STPAPIRequestTest.m in Sources */, F1D96F9B1DC7DCDE00477E64 /* STPLocalizationUtils+STPTestAdditions.m in Sources */, 04415C6F1A6605B5001225ED /* STPCertTest.m in Sources */, 04415C701A6605B5001225ED /* STPTokenTest.m in Sources */, 8B8DDBB31EF887A4004B141F /* STPBankAccountParamsTest.m in Sources */, + B64763B922FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m in Sources */, B3C9CF2D2004595A005502ED /* STPConnectAccountFunctionalTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4074,6 +4088,7 @@ 045D712F1CF4ED7600F6CD65 /* STPBINRange.m in Sources */, F148ABCA1D5D334B0014FD92 /* STPLocalizationUtils.m in Sources */, 045D71111CEEE30500F6CD65 /* STPAspects.m in Sources */, + B64763B722FE193900C01BC0 /* STPSetupIntentLastSetupError.m in Sources */, 04F94DCA1D22A20D004FC826 /* STPSwitchTableViewCell.m in Sources */, 04CDE5BA1BC1F1F100548833 /* STPCardParams.m in Sources */, 0451CC471C49AE1C003B2CA6 /* STPPaymentResult.m in Sources */, @@ -4176,6 +4191,7 @@ B61C996622BBFA12004980FD /* STPAppInfo.m in Sources */, F1D3A24F1EB012010095BFA9 /* STPMultipartFormDataPart.m in Sources */, 04827D121D2575C6002DB3E8 /* STPImageLibrary.m in Sources */, + B64763B622FE193800C01BC0 /* STPSetupIntentLastSetupError.m in Sources */, 04CDB5041A5F30A700B854EE /* STPFormEncoder.m in Sources */, 0426B96F1CEADC98006AC8DD /* STPColorUtils.m in Sources */, C1D7B51C1E36B8B9002181F5 /* STPSourceParams.m in Sources */, diff --git a/Stripe/Payments/STPPaymentHandler.m b/Stripe/Payments/STPPaymentHandler.m index 0708156dcff..64bbc9fcfe0 100644 --- a/Stripe/Payments/STPPaymentHandler.m +++ b/Stripe/Payments/STPPaymentHandler.m @@ -25,6 +25,7 @@ #import "STPIntentActionUseStripeSDK.h" #import "STPSetupIntent.h" #import "STPSetupIntentConfirmParams.h" +#import "STPSetupIntentLastSetupError.h" #import "STPThreeDSCustomizationSettings.h" #import "STPThreeDSCustomization+Private.h" #import "STPURLCallbackHandler.h" @@ -341,8 +342,17 @@ - (BOOL)_handleSetupIntentStatusForAction:(STPPaymentHandlerSetupIntentActionPar [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerIntentStatusErrorCode userInfo:@{@"STPSetupIntent": setupIntent.description}]]; case STPSetupIntentStatusRequiresPaymentMethod: // If the user forgot to attach a PaymentMethod, they get an error before this point. - // If authentication fails, or the card is declined, the SetupIntent transitions to this state. - [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; + // If confirmation fails (eg not authenticated, card declined) the SetupIntent transitions to this state. + if ([setupIntent.lastSetupError.code isEqualToString:@"setup_intent_authentication_failure"]) { + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; + } else if (setupIntent.lastSetupError.type == STPSetupIntentLastSetupErrorTypeCard) { + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:@{NSLocalizedDescriptionKey: setupIntent.lastSetupError.message}]]; + } else { + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:nil]]; + } break; case STPSetupIntentStatusRequiresConfirmation: [action completeWithStatus:STPPaymentHandlerActionStatusSucceeded error:nil]; @@ -376,25 +386,20 @@ - (BOOL)_handlePaymentIntentStatusForAction:(STPPaymentHandlerPaymentIntentActio [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerIntentStatusErrorCode userInfo:@{@"STPPaymentIntent": paymentIntent.description}]]; break; - case STPPaymentIntentStatusRequiresPaymentMethod: { + case STPPaymentIntentStatusRequiresPaymentMethod: // If the user forgot to attach a PaymentMethod, they get an error before this point. // If confirmation fails (eg not authenticated, card declined) the PaymentIntent transitions to this state. - switch (paymentIntent.lastPaymentError.type) { - case STPPaymentIntentLastPaymentErrorTypeAuthentication: - [action completeWithStatus:STPPaymentHandlerActionStatusFailed - error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; - break; - case STPPaymentIntentLastPaymentErrorTypeCard: - [action completeWithStatus:STPPaymentHandlerActionStatusFailed - error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:@{NSLocalizedDescriptionKey: paymentIntent.lastPaymentError.message}]]; - break; - default: - [action completeWithStatus:STPPaymentHandlerActionStatusFailed - error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:nil]]; - break; + if ([paymentIntent.lastPaymentError.code isEqualToString:@"payment_intent_authentication_failure"]) { + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; + } else if (paymentIntent.lastPaymentError.type == STPPaymentIntentLastPaymentErrorTypeCard) { + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:@{NSLocalizedDescriptionKey: paymentIntent.lastPaymentError.message}]]; + } else { + [action completeWithStatus:STPPaymentHandlerActionStatusFailed + error:[self _errorForCode:STPPaymentHandlerPaymentErrorCode userInfo:nil]]; } break; - } case STPPaymentIntentStatusRequiresConfirmation: [action completeWithStatus:STPPaymentHandlerActionStatusSucceeded error:nil]; break; @@ -778,7 +783,7 @@ - (void)_markChallengeCompletedWithCompletion:(STPBooleanSuccessBlock)completion - (NSError *)_errorForCode:(STPPaymentHandlerErrorCode)errorCode userInfo:(nullable NSDictionary *)additionalUserInfo { NSMutableDictionary *userInfo = additionalUserInfo ? [additionalUserInfo mutableCopy] : [NSMutableDictionary new]; switch (errorCode) { - // 3DS2 flow expected user errors + // 3DS(2) flow expected user errors case STPPaymentHandlerNotAuthenticatedErrorCode: userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"We are unable to authenticate your payment method. Please choose a different payment method and try again.", @"Error when 3DS2 authentication failed (e.g. customer entered the wrong code)"); break; diff --git a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h index d07b2fde7db..2ebfad63710 100644 --- a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h +++ b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h @@ -1,5 +1,5 @@ // -// STPPaymentIntentLastError.h +// STPPaymentIntentLastPaymentError.h // Stripe // // Created by Yuki Tokuhiro on 8/8/19. @@ -13,9 +13,9 @@ @class STPPaymentMethod; /** - The type of the error represented by `STPPaymentIntentLastError`. + The type of the error represented by `STPPaymentIntentLastPaymentError`. - Some STPPaymentIntentLastError properties are only populated for certain error types. + Some STPPaymentIntentLastPaymentError properties are only populated for certain error types. */ typedef NS_ENUM(NSUInteger, STPPaymentIntentLastPaymentErrorType) { /** @@ -66,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN /** The payment error encountered in the previous PaymentIntent confirmation. + + @see https://stripe.com/docs/api/payment_intents/object#payment_intent_object-last_payment_error */ @interface STPPaymentIntentLastPaymentError : NSObject diff --git a/Stripe/PublicHeaders/STPSetupIntent.h b/Stripe/PublicHeaders/STPSetupIntent.h index 021700c940c..34d9ee50b9a 100644 --- a/Stripe/PublicHeaders/STPSetupIntent.h +++ b/Stripe/PublicHeaders/STPSetupIntent.h @@ -11,7 +11,7 @@ #import "STPAPIResponseDecodable.h" #import "STPSetupIntentEnums.h" -@class STPIntentAction; +@class STPIntentAction, STPSetupIntentLastSetupError; NS_ASSUME_NONNULL_BEGIN @@ -84,6 +84,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly) STPSetupIntentUsage usage; +/** + The setup error encountered in the previous SetupIntent confirmation. + */ +@property (nonatomic, nullable, readonly) STPSetupIntentLastSetupError *lastSetupError; + @end NS_ASSUME_NONNULL_END diff --git a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h new file mode 100644 index 00000000000..5a77099d545 --- /dev/null +++ b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h @@ -0,0 +1,119 @@ +// +// STPSetupIntentLastSetupError.h +// Stripe +// +// Created by Yuki Tokuhiro on 8/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +#import + +#import "STPAPIResponseDecodable.h" + +@class STPPaymentMethod; + +/** + The type of the error represented by `STPSetupIntentLastSetupError`. + + Some STPSetupIntentLastError properties are only populated for certain error types. + */ +typedef NS_ENUM(NSUInteger, STPSetupIntentLastSetupErrorType) { + /** + An unknown error type. + */ + STPSetupIntentLastSetupErrorTypeUnknown, + + /** + An error connecting to Stripe's API. + */ + STPSetupIntentLastSetupErrorTypeAPIConnection, + + /** + An error with the Stripe API. + */ + STPSetupIntentLastSetupErrorTypeAPI, + + /** + A failure to authenticate your customer. + */ + STPSetupIntentLastSetupErrorTypeAuthentication, + + /* + Card errors are the most common type of error you should expect to handle. + They result when the user enters a card that can't be charged for some reason. + + Check the `declineCode` property for the decline code. The `message` property contains a message you can show to your users. + */ + STPSetupIntentLastSetupErrorTypeCard, + + /** + Keys for idempotent requests can only be used with the same parameters they were first used with. + */ + STPSetupIntentLastSetupErrorTypeIdempotency, + + /** + Invalid request errors. Typically, this is because your request has invalid parameters. + */ + STPSetupIntentLastSetupErrorTypeInvalidRequest, + + /** + Too many requests hit the API too quickly. + */ + STPSetupIntentLastSetupErrorTypeRateLimit, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + The error encountered in the previous SetupIntent confirmation. + + @see https://stripe.com/docs/api/setup_intents/object#setup_intent_object-last_setup_error +*/ +@interface STPSetupIntentLastSetupError : NSObject + +/** + For some errors that could be handled programmatically, a short string indicating the error code reported. + + @see https://stripe.com/docs/error-codes + */ +@property (nonatomic, nullable, readonly) NSString *code; + +/** + For card errors resulting from a card issuer decline, a short string indicating the card issuer’s reason for the decline if they provide one. + + @see https://stripe.com/docs/declines#issuer-declines + */ +@property (nonatomic, nullable, readonly) NSString *declineCode; + +/** + A URL to more information about the error code reported. + + @see https://stripe.com/docs/error-codes + */ +@property (nonatomic, readonly) NSString *docURL; + +/** + A human-readable message providing more details about the error. + For card errors, these messages can be shown to your users. + */ +@property (nonatomic, readonly) NSString *message; + +/** + If the error is parameter-specific, the parameter related to the error. + For example, you can use this to display a message near the correct form field. + */ +@property (nonatomic, nullable, readonly) NSString *param; + +/** + The PaymentMethod object for errors returned on a request involving a PaymentMethod. + */ +@property (nonatomic, nullable, readonly) STPPaymentMethod *paymentMethod; + +/** + The type of error returned. + */ +@property (nonatomic, readonly) STPSetupIntentLastSetupErrorType type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe/PublicHeaders/Stripe.h b/Stripe/PublicHeaders/Stripe.h index e2f63b08746..58d0b8a2911 100644 --- a/Stripe/PublicHeaders/Stripe.h +++ b/Stripe/PublicHeaders/Stripe.h @@ -74,6 +74,7 @@ #import "STPSetupIntent.h" #import "STPSetupIntentConfirmParams.h" #import "STPSetupIntentEnums.h" +#import "STPSetupIntentLastSetupError.h" #import "STPShippingAddressViewController.h" #import "STPSource.h" #import "STPSourceCardDetails.h" diff --git a/Stripe/STPPaymentIntentLastPaymentError.m b/Stripe/STPPaymentIntentLastPaymentError.m index 3dd2ac65274..ad68fcc01b1 100644 --- a/Stripe/STPPaymentIntentLastPaymentError.m +++ b/Stripe/STPPaymentIntentLastPaymentError.m @@ -1,5 +1,5 @@ // -// STPPaymentIntentLastError.m +// STPPaymentIntentLastPaymentError.m // Stripe // // Created by Yuki Tokuhiro on 8/8/19. diff --git a/Stripe/STPSetupIntent.m b/Stripe/STPSetupIntent.m index e54fd77800b..c33b927da33 100644 --- a/Stripe/STPSetupIntent.m +++ b/Stripe/STPSetupIntent.m @@ -10,6 +10,7 @@ #import "STPIntentAction.h" #import "STPPaymentMethod+Private.h" +#import "STPSetupIntentLastSetupError.h" #import "NSArray+Stripe.h" #import "NSDictionary+Stripe.h" @@ -27,6 +28,7 @@ @interface STPSetupIntent() @property (nonatomic, copy) NSArray *paymentMethodTypes; @property (nonatomic) STPSetupIntentStatus status; @property (nonatomic) STPSetupIntentUsage usage; +@property (nonatomic, nullable, readwrite) STPSetupIntentLastSetupError *lastSetupError; @property (nonatomic, copy, nonnull, readwrite) NSDictionary *allResponseFields; @end @@ -46,6 +48,7 @@ - (NSString *)description { [NSString stringWithFormat:@"created = %@", self.created], [NSString stringWithFormat:@"customerId = %@", self.customerID], [NSString stringWithFormat:@"description = %@", self.stripeDescription], + [NSString stringWithFormat:@"lastSetupError = %@", self.lastSetupError], [NSString stringWithFormat:@"livemode = %@", self.livemode ? @"YES" : @"NO"], [NSString stringWithFormat:@"metadata = %@", self.metadata], [NSString stringWithFormat:@"nextAction = %@", self.nextAction], @@ -131,6 +134,7 @@ + (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)r setupIntent.status = [[self class] statusFromString:rawStatus]; NSString *rawUsage = [dict stp_stringForKey:@"usage"]; setupIntent.usage = rawUsage ? [[self class] usageFromString:rawUsage] : STPSetupIntentUsageNone; + setupIntent.lastSetupError = [STPSetupIntentLastSetupError decodedObjectFromAPIResponse:[dict stp_dictionaryForKey:@"last_setup_error"]]; setupIntent.allResponseFields = dict; diff --git a/Stripe/STPSetupIntentLastSetupError.m b/Stripe/STPSetupIntentLastSetupError.m new file mode 100644 index 00000000000..fa2b02496d9 --- /dev/null +++ b/Stripe/STPSetupIntentLastSetupError.m @@ -0,0 +1,81 @@ +// +// STPSetupIntentLastSetupError.m +// Stripe +// +// Created by Yuki Tokuhiro on 8/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +#import "STPSetupIntentLastSetupError.h" + +#import "NSDictionary+Stripe.h" +#import "STPPaymentMethod.h" + +@interface STPSetupIntentLastSetupError() +@property (nonatomic, copy) NSString *code; +@property (nonatomic, copy) NSString *declineCode; +@property (nonatomic, copy) NSString *docURL; +@property (nonatomic, copy) NSString *message; +@property (nonatomic, copy) NSString *param; +@property (nonatomic) STPPaymentMethod *paymentMethod; +@property (nonatomic) STPSetupIntentLastSetupErrorType type; +@property (nonatomic, copy, nonnull, readwrite) NSDictionary *allResponseFields; +@end + +@implementation STPSetupIntentLastSetupError +- (NSString *)description { + NSArray *props = @[ + // Object + [NSString stringWithFormat:@"%@: %p", NSStringFromClass([self class]), self], + + // SetupIntentLastError details (alphabetical) + [NSString stringWithFormat:@"code = %@", self.code], + [NSString stringWithFormat:@"declineCode = %@", self.declineCode], + [NSString stringWithFormat:@"docURL = %@", self.docURL], + [NSString stringWithFormat:@"message = %@", self.message], + [NSString stringWithFormat:@"param = %@", self.param], + [NSString stringWithFormat:@"paymentMethod = %@", self.paymentMethod], + [NSString stringWithFormat:@"type = %@", self.allResponseFields[@"type"]], + ]; + + return [NSString stringWithFormat:@"<%@>", [props componentsJoinedByString:@"; "]]; +} + ++ (STPSetupIntentLastSetupErrorType)typeFromString:(NSString *)string { + NSDictionary *map = @{ + @"api_connection_error": @(STPSetupIntentLastSetupErrorTypeAPIConnection), + @"api_error": @(STPSetupIntentLastSetupErrorTypeAPI), + @"authentication_error": @(STPSetupIntentLastSetupErrorTypeAuthentication), + @"card_error": @(STPSetupIntentLastSetupErrorTypeCard), + @"idempotency_error": @(STPSetupIntentLastSetupErrorTypeIdempotency), + @"invalid_request_error": @(STPSetupIntentLastSetupErrorTypeInvalidRequest), + @"rate_limit_error": @(STPSetupIntentLastSetupErrorTypeRateLimit), + }; + + NSString *key = string.lowercaseString; + NSNumber *statusNumber = map[key] ?: @(STPSetupIntentLastSetupErrorTypeUnknown); + return statusNumber.integerValue; +} + +#pragma mark - STPAPIResponseDecodable + ++ (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNulls]; + if (!dict) { + return nil; + } + + STPSetupIntentLastSetupError *lastError = [self new]; + lastError.code = [dict stp_stringForKey:@"code"]; + lastError.declineCode = [dict stp_stringForKey:@"declineCode"]; + lastError.docURL = [dict stp_stringForKey:@"doc_url"]; + lastError.message = [dict stp_stringForKey:@"message"]; + lastError.param = [dict stp_stringForKey:@"param"]; + lastError.paymentMethod = [STPPaymentMethod decodedObjectFromAPIResponse:[dict stp_dictionaryForKey:@"payment_method"]]; + lastError.type = [self typeFromString:[dict stp_stringForKey:@"type"]]; + lastError.allResponseFields = dict; + + return lastError; +} + +@end diff --git a/Tests/Tests/STPPaymentIntentLastErrorTest.m b/Tests/Tests/STPPaymentIntentLastpaymentErrorTest.m similarity index 94% rename from Tests/Tests/STPPaymentIntentLastErrorTest.m rename to Tests/Tests/STPPaymentIntentLastpaymentErrorTest.m index b3eef1dffe2..e392bdcb048 100644 --- a/Tests/Tests/STPPaymentIntentLastErrorTest.m +++ b/Tests/Tests/STPPaymentIntentLastpaymentErrorTest.m @@ -1,5 +1,5 @@ // -// STPPaymentIntentLastErrorTest.m +// STPPaymentIntentLastPaymentErrorTest.m // StripeiOS Tests // // Created by Yuki Tokuhiro on 8/8/19. @@ -16,11 +16,11 @@ @interface STPPaymentIntentLastPaymentError (Testing) + (STPPaymentIntentLastPaymentErrorType)typeFromString:(NSString *)string; @end -@interface STPPaymentIntentLastErrorTest : XCTestCase +@interface STPPaymentIntentLastpaymentErrorTest : XCTestCase @end -@implementation STPPaymentIntentLastErrorTest +@implementation STPPaymentIntentLastpaymentErrorTest - (void)testTypeFromString { XCTAssertEqual([STPPaymentIntentLastPaymentError typeFromString:@"api_connection_error"], STPPaymentIntentLastPaymentErrorTypeAPIConnection); diff --git a/Tests/Tests/STPPaymentIntentTest.m b/Tests/Tests/STPPaymentIntentTest.m index eb76659867d..dcfd24e0d89 100644 --- a/Tests/Tests/STPPaymentIntentTest.m +++ b/Tests/Tests/STPPaymentIntentTest.m @@ -221,7 +221,7 @@ - (void)testDecodedObjectFromAPIResponseMapping { XCTAssertEqualObjects(paymentIntent.lastPaymentError.docURL, @"https://stripe.com/docs/error-codes/payment-intent-authentication-failure"); XCTAssertEqualObjects(paymentIntent.lastPaymentError.message, @"The provided PaymentMethod has failed authentication. You can provide payment_method_data or a new PaymentMethod to attempt to fulfill this PaymentIntent again."); XCTAssertNotNil(paymentIntent.lastPaymentError.paymentMethod); - XCTAssertEqual(paymentIntent.lastPaymentError.type, STPPaymentIntentLastErrorTypeInvalidRequest); + XCTAssertEqual(paymentIntent.lastPaymentError.type, STPPaymentIntentLastPaymentErrorTypeInvalidRequest); XCTAssertNotEqual(paymentIntent.allResponseFields, response, @"should have own copy of fields"); XCTAssertEqualObjects(paymentIntent.allResponseFields, response, @"fields values should match"); diff --git a/Tests/Tests/STPSetupIntentLastSetupErrorTest.m b/Tests/Tests/STPSetupIntentLastSetupErrorTest.m new file mode 100644 index 00000000000..504b99fc716 --- /dev/null +++ b/Tests/Tests/STPSetupIntentLastSetupErrorTest.m @@ -0,0 +1,47 @@ +// +// STPSetupIntentLastSetupErrorTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 8/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +#import + +#import +#import "STPTestUtils.h" +#import "STPFixtures.h" + +@interface STPSetupIntentLastSetupError (Testing) ++ (STPSetupIntentLastSetupErrorType)typeFromString:(NSString *)string; +@end + +@interface STPSetupIntentLastSetupErrorTest : XCTestCase + +@end + +@implementation STPSetupIntentLastSetupErrorTest + +- (void)testTypeFromString { + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"api_connection_error"], STPSetupIntentLastSetupErrorTypeAPIConnection); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"API_CONNECTION_ERROR"], STPSetupIntentLastSetupErrorTypeAPIConnection); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"api_error"], STPSetupIntentLastSetupErrorTypeAPI); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"API_ERROR"], STPSetupIntentLastSetupErrorTypeAPI); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"authentication_error"], STPSetupIntentLastSetupErrorTypeAuthentication); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"AUTHENTICATION_ERROR"], STPSetupIntentLastSetupErrorTypeAuthentication); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"card_error"], STPSetupIntentLastSetupErrorTypeCard); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"CARD_ERROR"], STPSetupIntentLastSetupErrorTypeCard); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"idempotency_error"], STPSetupIntentLastSetupErrorTypeIdempotency); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"IDEMPOTENCY_ERROR"], STPSetupIntentLastSetupErrorTypeIdempotency); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"invalid_request_error"], STPSetupIntentLastSetupErrorTypeInvalidRequest); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"INVALID_REQUEST_ERROR"], STPSetupIntentLastSetupErrorTypeInvalidRequest); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"rate_limit_error"], STPSetupIntentLastSetupErrorTypeRateLimit); + XCTAssertEqual([STPSetupIntentLastSetupError typeFromString:@"RATE_LIMIT_ERROR"], STPSetupIntentLastSetupErrorTypeRateLimit); +} + +#pragma mark - STPAPIResponseDecodable Tests + +// STPSetupIntentLastError is a sub-object of STPSetupIntent, see STPSetupIntentTest + + +@end diff --git a/Tests/Tests/STPSetupIntentTest.m b/Tests/Tests/STPSetupIntentTest.m index 2873460298b..8b1fafa41cb 100644 --- a/Tests/Tests/STPSetupIntentTest.m +++ b/Tests/Tests/STPSetupIntentTest.m @@ -83,6 +83,15 @@ - (void)testDecodedObjectFromAPIResponseMapping { XCTAssertEqualObjects(setupIntent.paymentMethodTypes, @[@(STPPaymentMethodTypeCard)]); + // lastSetupError + + XCTAssertNotNil(setupIntent.lastSetupError); + XCTAssertEqualObjects(setupIntent.lastSetupError.code, @"setup_intent_authentication_failure"); + XCTAssertEqualObjects(setupIntent.lastSetupError.docURL, @"https://stripe.com/docs/error-codes/setup-intent-authentication-failure"); + XCTAssertEqualObjects(setupIntent.lastSetupError.message, @"The latest attempt to set up the payment method has failed because authentication failed."); + XCTAssertNotNil(setupIntent.lastSetupError.paymentMethod); + XCTAssertEqual(setupIntent.lastSetupError.type, STPSetupIntentLastSetupErrorTypeInvalidRequest); + XCTAssertNotEqual(setupIntent.allResponseFields, response, @"should have own copy of fields"); } diff --git a/Tests/Tests/SetupIntent.json b/Tests/Tests/SetupIntent.json index ce06763b6d1..5eb5959cb68 100644 --- a/Tests/Tests/SetupIntent.json +++ b/Tests/Tests/SetupIntent.json @@ -6,7 +6,53 @@ "created": 123456789, "customer": "cus_123456", "description": "My Sample SetupIntent", - "last_setup_error": null, + "last_setup_error": { + "code": "setup_intent_authentication_failure", + "doc_url": "https://stripe.com/docs/error-codes/setup-intent-authentication-failure", + "message": "The latest attempt to set up the payment method has failed because authentication failed.", + "payment_method": { + "id": "pm_1F5fdSKlwPmebFhpPTD7Tg4l", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": null, + "exp_month": 2, + "exp_year": 2042, + "funding": "credit", + "generated_from": null, + "last4": "3246", + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1565386110, + "customer": null, + "livemode": false, + "metadata": { + }, + "type": "card" + }, + "type": "invalid_request_error" + }, "livemode": false, "metadata": { "user_id": "guest_1234567" From 152113e937a2d6ff136269b8c1daab62e9aab757 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Fri, 9 Aug 2019 15:17:47 -0700 Subject: [PATCH 3/6] Fix up .h --- Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h | 9 +++++---- Stripe/PublicHeaders/STPSetupIntentLastSetupError.h | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h index 2ebfad63710..029cb7ba2bc 100644 --- a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h +++ b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h @@ -38,7 +38,7 @@ typedef NS_ENUM(NSUInteger, STPPaymentIntentLastPaymentErrorType) { */ STPPaymentIntentLastPaymentErrorTypeAuthentication, - /* + /** Card errors are the most common type of error you should expect to handle. They result when the user enters a card that can't be charged for some reason. @@ -79,7 +79,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) NSString *code; /** - For card errors resulting from a card issuer decline, a short string indicating the card issuer’s reason for the decline if they provide one. + For card (`STPPaymentIntentLastPaymentErrorTypeCard`) errors resulting from a card issuer decline, + a short string indicating the card issuer’s reason for the decline if they provide one. @see https://stripe.com/docs/declines#issuer-declines */ @@ -94,7 +95,7 @@ NS_ASSUME_NONNULL_BEGIN /** A human-readable message providing more details about the error. - For card errors, these messages can be shown to your users. + For card (`STPPaymentIntentLastPaymentErrorTypeCard`) errors, these messages can be shown to your users. */ @property (nonatomic, readonly) NSString *message; @@ -110,7 +111,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) STPPaymentMethod *paymentMethod; /** - The type of error returned. + The type of error. */ @property (nonatomic, readonly) STPPaymentIntentLastPaymentErrorType type; diff --git a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h index 5a77099d545..03f44105008 100644 --- a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h +++ b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h @@ -38,7 +38,7 @@ typedef NS_ENUM(NSUInteger, STPSetupIntentLastSetupErrorType) { */ STPSetupIntentLastSetupErrorTypeAuthentication, - /* + /** Card errors are the most common type of error you should expect to handle. They result when the user enters a card that can't be charged for some reason. @@ -79,7 +79,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) NSString *code; /** - For card errors resulting from a card issuer decline, a short string indicating the card issuer’s reason for the decline if they provide one. + For card (`STPSetupIntentLastSetupErrorTypeCard`) errors resulting from a card issuer decline, + a short string indicating the card issuer’s reason for the decline if they provide one. @see https://stripe.com/docs/declines#issuer-declines */ @@ -94,7 +95,7 @@ NS_ASSUME_NONNULL_BEGIN /** A human-readable message providing more details about the error. - For card errors, these messages can be shown to your users. + For card (`STPSetupIntentLastSetupErrorTypeCard`) errors, these messages can be shown to your users. */ @property (nonatomic, readonly) NSString *message; @@ -110,7 +111,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) STPPaymentMethod *paymentMethod; /** - The type of error returned. + The type of error. */ @property (nonatomic, readonly) STPSetupIntentLastSetupErrorType type; From 22e1662d793624dff57e54ef2c886c0919e12d03 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Mon, 19 Aug 2019 10:19:07 -0700 Subject: [PATCH 4/6] AuthenticationFailure error code const --- Stripe/Payments/STPPaymentHandler.m | 4 ++-- Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h | 2 ++ Stripe/PublicHeaders/STPSetupIntentLastSetupError.h | 4 ++++ Stripe/STPPaymentIntentLastPaymentError.m | 2 ++ Stripe/STPSetupIntentLastSetupError.m | 2 ++ 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Stripe/Payments/STPPaymentHandler.m b/Stripe/Payments/STPPaymentHandler.m index 64bbc9fcfe0..751c2a0d45d 100644 --- a/Stripe/Payments/STPPaymentHandler.m +++ b/Stripe/Payments/STPPaymentHandler.m @@ -343,7 +343,7 @@ - (BOOL)_handleSetupIntentStatusForAction:(STPPaymentHandlerSetupIntentActionPar case STPSetupIntentStatusRequiresPaymentMethod: // If the user forgot to attach a PaymentMethod, they get an error before this point. // If confirmation fails (eg not authenticated, card declined) the SetupIntent transitions to this state. - if ([setupIntent.lastSetupError.code isEqualToString:@"setup_intent_authentication_failure"]) { + if ([setupIntent.lastSetupError.code isEqualToString:STPSetupIntentLastSetupErrorCodeAuthenticationFailure]) { [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; } else if (setupIntent.lastSetupError.type == STPSetupIntentLastSetupErrorTypeCard) { @@ -389,7 +389,7 @@ - (BOOL)_handlePaymentIntentStatusForAction:(STPPaymentHandlerPaymentIntentActio case STPPaymentIntentStatusRequiresPaymentMethod: // If the user forgot to attach a PaymentMethod, they get an error before this point. // If confirmation fails (eg not authenticated, card declined) the PaymentIntent transitions to this state. - if ([paymentIntent.lastPaymentError.code isEqualToString:@"payment_intent_authentication_failure"]) { + if ([paymentIntent.lastPaymentError.code isEqualToString:STPPaymentIntentLastPaymentErrorCodeAuthenticationFailure]) { [action completeWithStatus:STPPaymentHandlerActionStatusFailed error:[self _errorForCode:STPPaymentHandlerNotAuthenticatedErrorCode userInfo:nil]]; } else if (paymentIntent.lastPaymentError.type == STPPaymentIntentLastPaymentErrorTypeCard) { diff --git a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h index 029cb7ba2bc..a1a95c3ee1d 100644 --- a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h +++ b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h @@ -64,6 +64,8 @@ typedef NS_ENUM(NSUInteger, STPPaymentIntentLastPaymentErrorType) { NS_ASSUME_NONNULL_BEGIN +extern NSString *const STPPaymentIntentLastPaymentErrorCodeAuthenticationFailure; + /** The payment error encountered in the previous PaymentIntent confirmation. diff --git a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h index 03f44105008..18186ff5f89 100644 --- a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h +++ b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h @@ -64,6 +64,10 @@ typedef NS_ENUM(NSUInteger, STPSetupIntentLastSetupErrorType) { NS_ASSUME_NONNULL_BEGIN +#pragma mark - Error Codes + +extern NSString *const STPSetupIntentLastSetupErrorCodeAuthenticationFailure; + /** The error encountered in the previous SetupIntent confirmation. diff --git a/Stripe/STPPaymentIntentLastPaymentError.m b/Stripe/STPPaymentIntentLastPaymentError.m index ad68fcc01b1..37acf18cb8a 100644 --- a/Stripe/STPPaymentIntentLastPaymentError.m +++ b/Stripe/STPPaymentIntentLastPaymentError.m @@ -11,6 +11,8 @@ #import "NSDictionary+Stripe.h" #import "STPPaymentMethod.h" +NSString *const STPPaymentIntentLastPaymentErrorCodeAuthenticationFailure = @"payment_intent_authentication_failure"; + @interface STPPaymentIntentLastPaymentError() @property (nonatomic, copy) NSString *code; @property (nonatomic, copy) NSString *declineCode; diff --git a/Stripe/STPSetupIntentLastSetupError.m b/Stripe/STPSetupIntentLastSetupError.m index fa2b02496d9..e93f921e862 100644 --- a/Stripe/STPSetupIntentLastSetupError.m +++ b/Stripe/STPSetupIntentLastSetupError.m @@ -11,6 +11,8 @@ #import "NSDictionary+Stripe.h" #import "STPPaymentMethod.h" +NSString *const STPSetupIntentLastSetupErrorCodeAuthenticationFailure = @"setup_intent_authentication_failure"; + @interface STPSetupIntentLastSetupError() @property (nonatomic, copy) NSString *code; @property (nonatomic, copy) NSString *declineCode; From de9cc0a47d9d07c8c3e146e938f3a306300e886b Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Mon, 19 Aug 2019 10:19:29 -0700 Subject: [PATCH 5/6] camel case STPPaymentIntentLastpaymentErrorTest.m --- Stripe.xcodeproj/project.pbxproj | 8 ++++---- ...ErrorTest.m => STPPaymentIntentLastPaymentErrorTest.m} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename Tests/Tests/{STPPaymentIntentLastpaymentErrorTest.m => STPPaymentIntentLastPaymentErrorTest.m} (100%) diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj index 6702d2f9b98..1e36de2e0d0 100644 --- a/Stripe.xcodeproj/project.pbxproj +++ b/Stripe.xcodeproj/project.pbxproj @@ -671,7 +671,7 @@ B6A46F7722FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h in Headers */ = {isa = PBXBuildFile; fileRef = B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */; settings = {ATTRIBUTES = (Public, ); }; }; B6A46F7822FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */; }; B6A46F7922FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */; }; - B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m */; }; + B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastPaymentErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastPaymentErrorTest.m */; }; B6B41F71223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B41F70223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m */; }; B6B41F73223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B41F72223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m */; }; B6B41F75223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = B6B41F74223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h */; }; @@ -1623,7 +1623,7 @@ B69FEC41222EE9E000273A16 /* STPPaymentMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STPPaymentMethod.h; path = PublicHeaders/STPPaymentMethod.h; sourceTree = ""; }; B6A46F7422FCDB81001991B2 /* STPPaymentIntentLastPaymentError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = STPPaymentIntentLastPaymentError.h; path = PublicHeaders/STPPaymentIntentLastPaymentError.h; sourceTree = ""; }; B6A46F7522FCDB81001991B2 /* STPPaymentIntentLastPaymentError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastPaymentError.m; sourceTree = ""; }; - B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastpaymentErrorTest.m; sourceTree = ""; }; + B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastPaymentErrorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentIntentLastPaymentErrorTest.m; sourceTree = ""; }; B6B41F70223476AE0020BA7F /* STPPaymentMethodCardWalletMasterpassTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodCardWalletMasterpassTest.m; sourceTree = ""; }; B6B41F72223476B90020BA7F /* STPPaymentMethodCardWalletVisaCheckoutTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodCardWalletVisaCheckoutTest.m; sourceTree = ""; }; B6B41F74223481BA0020BA7F /* STPPaymentMethodCardWallet+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STPPaymentMethodCardWallet+Private.h"; sourceTree = ""; }; @@ -2321,7 +2321,7 @@ isa = PBXGroup; children = ( B64763B822FE1AF700C01BC0 /* STPSetupIntentLastSetupErrorTest.m */, - B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m */, + B6A46F7A22FCE579001991B2 /* STPPaymentIntentLastPaymentErrorTest.m */, 04A4C3911C4F263300B3B290 /* NSArray+StripeTest.m */, C11810981CC6D46D0022FB55 /* NSDecimalNumber+StripeTest.m */, 8BB97F071F26645B0095122A /* NSDictionary+StripeTest.m */, @@ -3958,7 +3958,7 @@ C1080F4C1CBED48A007B2D89 /* STPAddressTests.m in Sources */, C1C02CCE1ECCE92900DF5643 /* STPEphemeralKeyTest.m in Sources */, 36D4EA6122DD33DF00619BA8 /* STPSetupIntentConfirmParamsTest.m in Sources */, - B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastpaymentErrorTest.m in Sources */, + B6A46F7B22FCE579001991B2 /* STPPaymentIntentLastPaymentErrorTest.m in Sources */, B36C6D782193A16F00D17575 /* STPIntentActionTest.m in Sources */, C14C4DB11EC3B34500C2FDF6 /* STPAPIRequestTest.m in Sources */, F1D96F9B1DC7DCDE00477E64 /* STPLocalizationUtils+STPTestAdditions.m in Sources */, diff --git a/Tests/Tests/STPPaymentIntentLastpaymentErrorTest.m b/Tests/Tests/STPPaymentIntentLastPaymentErrorTest.m similarity index 100% rename from Tests/Tests/STPPaymentIntentLastpaymentErrorTest.m rename to Tests/Tests/STPPaymentIntentLastPaymentErrorTest.m From de96c46c459f98fbb2313de62f955dcf2dfb3534 Mon Sep 17 00:00:00 2001 From: Yuki Tokuhiro Date: Mon, 19 Aug 2019 16:04:42 -0700 Subject: [PATCH 6/6] Add docstring --- Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h | 3 +++ Stripe/PublicHeaders/STPSetupIntentLastSetupError.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h index a1a95c3ee1d..5fe5944e559 100644 --- a/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h +++ b/Stripe/PublicHeaders/STPPaymentIntentLastPaymentError.h @@ -64,6 +64,9 @@ typedef NS_ENUM(NSUInteger, STPPaymentIntentLastPaymentErrorType) { NS_ASSUME_NONNULL_BEGIN +/** + A value for `code` indicating the provided payment method failed authentication. + */ extern NSString *const STPPaymentIntentLastPaymentErrorCodeAuthenticationFailure; /** diff --git a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h index 18186ff5f89..97f14929dbd 100644 --- a/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h +++ b/Stripe/PublicHeaders/STPSetupIntentLastSetupError.h @@ -66,6 +66,9 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Error Codes +/** + A value for `code` indicating the provided payment method failed authentication. + */ extern NSString *const STPSetupIntentLastSetupErrorCodeAuthenticationFailure; /**