BTCardClient.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #import "BTCardClient_Internal.h"
  2. #import "BTCardNonce_Internal.h"
  3. #import "BTCard_Internal.h"
  4. #import "BTConfiguration+Card.h"
  5. #if __has_include(<Braintree/BraintreeCard.h>) // CocoaPods
  6. #import <Braintree/BTCardRequest.h>
  7. #import <Braintree/BTAPIClient_Internal.h>
  8. #import <Braintree/BTPaymentMethodNonceParser.h>
  9. #import <Braintree/BraintreeCore.h>
  10. #elif SWIFT_PACKAGE // SPM
  11. #import <BraintreeCard/BTCardRequest.h>
  12. #import "../BraintreeCore/BTAPIClient_Internal.h"
  13. #import "../BraintreeCore/BTPaymentMethodNonceParser.h"
  14. #import <BraintreeCore/BraintreeCore.h>
  15. #else // Carthage
  16. #import <BraintreeCard/BTCardRequest.h>
  17. #import <BraintreeCore/BTAPIClient_Internal.h>
  18. #import <BraintreeCore/BTPaymentMethodNonceParser.h>
  19. #import <BraintreeCore/BraintreeCore.h>
  20. #endif
  21. NSString *const BTCardClientErrorDomain = @"com.braintreepayments.BTCardClientErrorDomain";
  22. NSString *const BTCardClientGraphQLTokenizeFeature = @"tokenize_credit_cards";
  23. @interface BTCardClient ()
  24. @end
  25. @implementation BTCardClient
  26. + (void)load {
  27. if (self == [BTCardClient class]) {
  28. [[BTPaymentMethodNonceParser sharedParser] registerType:@"CreditCard" withParsingBlock:^BTPaymentMethodNonce * _Nullable(BTJSON * _Nonnull creditCard) {
  29. return [BTCardNonce cardNonceWithJSON:creditCard];
  30. }];
  31. }
  32. }
  33. - (instancetype)initWithAPIClient:(BTAPIClient *)apiClient {
  34. if (!apiClient) {
  35. return nil;
  36. }
  37. if (self = [super init]) {
  38. self.apiClient = apiClient;
  39. }
  40. return self;
  41. }
  42. - (instancetype)init {
  43. return nil;
  44. }
  45. - (void)tokenizeCard:(BTCard *)card completion:(void (^)(BTCardNonce *tokenizedCard, NSError *error))completion {
  46. BTCardRequest *request = [[BTCardRequest alloc] initWithCard:card];
  47. #pragma clang diagnostic push
  48. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  49. [self tokenizeCard:request options:nil completion:completion];
  50. #pragma clang diagnostic pop
  51. }
  52. - (void)tokenizeCard:(BTCardRequest *)request options:(NSDictionary *)options completion:(void (^)(BTCardNonce * _Nullable, NSError * _Nullable))completionBlock
  53. {
  54. if (!self.apiClient) {
  55. NSError *error = [NSError errorWithDomain:BTCardClientErrorDomain
  56. code:BTCardClientErrorTypeIntegration
  57. userInfo:@{NSLocalizedDescriptionKey: @"BTCardClient tokenization failed because BTAPIClient is nil."}];
  58. completionBlock(nil, error);
  59. return;
  60. }
  61. [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable configuration, NSError * _Nullable error) {
  62. if (error) {
  63. completionBlock(nil, error);
  64. return;
  65. }
  66. // Union Pay tokenization requests should not go through the GraphQL API
  67. if ([self isGraphQLEnabledForCardTokenization:configuration] && !request.enrollmentID) {
  68. if (request.card.authenticationInsightRequested && !request.card.merchantAccountID) {
  69. NSError *error = [NSError errorWithDomain:BTCardClientErrorDomain
  70. code:BTCardClientErrorTypeIntegration
  71. userInfo:@{NSLocalizedDescriptionKey: @"BTCardClient tokenization failed because a merchant account ID is required when authenticationInsightRequested is true."}];
  72. completionBlock(nil, error);
  73. return;
  74. }
  75. NSDictionary *parameters = [request.card graphQLParameters];
  76. [self.apiClient POST:@""
  77. parameters:parameters
  78. httpType:BTAPIClientHTTPTypeGraphQLAPI
  79. completion:^(BTJSON * _Nullable body, __unused NSHTTPURLResponse * _Nullable response, NSError * _Nullable error)
  80. {
  81. if (error) {
  82. if (error.code == NETWORK_CONNECTION_LOST_CODE) {
  83. [self.apiClient sendAnalyticsEvent:@"ios.tokenize-card.graphQL.network-connection.failure"];
  84. }
  85. NSHTTPURLResponse *response = error.userInfo[BTHTTPURLResponseKey];
  86. NSError *callbackError = error;
  87. if (response.statusCode == 422) {
  88. if (error.userInfo) {
  89. callbackError = [self constructCallbackErrorForErrorUserInfo:error.userInfo error:error];
  90. }
  91. }
  92. [self sendGraphQLAnalyticsEventWithSuccess:NO];
  93. completionBlock(nil, callbackError);
  94. return;
  95. }
  96. BTJSON *cardJSON = body[@"data"][@"tokenizeCreditCard"];
  97. [self sendGraphQLAnalyticsEventWithSuccess:YES];
  98. BTCardNonce *cardNonce = [BTCardNonce cardNonceWithGraphQLJSON:cardJSON];
  99. completionBlock(cardNonce, cardJSON.asError);
  100. }];
  101. } else {
  102. NSDictionary *parameters = [self clientAPIParametersForCard:request options:options];
  103. [self.apiClient POST:@"v1/payment_methods/credit_cards"
  104. parameters:parameters
  105. completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error)
  106. {
  107. if (error != nil) {
  108. if (error.code == NETWORK_CONNECTION_LOST_CODE) {
  109. [self.apiClient sendAnalyticsEvent:@"ios.tokenize-card.network-connection.failure"];
  110. }
  111. NSHTTPURLResponse *response = error.userInfo[BTHTTPURLResponseKey];
  112. NSError *callbackError = error;
  113. if (response.statusCode == 422) {
  114. if (error.userInfo) {
  115. callbackError = [self constructCallbackErrorForErrorUserInfo:error.userInfo error:error];
  116. }
  117. }
  118. if (request.enrollmentID) {
  119. [self sendUnionPayAnalyticsEvent:NO];
  120. } else {
  121. [self sendAnalyticsEventWithSuccess:NO];
  122. }
  123. completionBlock(nil, callbackError);
  124. return;
  125. }
  126. BTJSON *cardJSON = body[@"creditCards"][0];
  127. if (request.enrollmentID) {
  128. [self sendUnionPayAnalyticsEvent:!cardJSON.isError];
  129. } else {
  130. [self sendAnalyticsEventWithSuccess:!cardJSON.isError];
  131. }
  132. // cardNonceWithJSON returns nil when cardJSON is nil, cardJSON.asError is nil when cardJSON is non-nil
  133. BTCardNonce *cardNonce = [BTCardNonce cardNonceWithJSON:cardJSON];
  134. completionBlock(cardNonce, cardJSON.asError);
  135. }];
  136. }
  137. }];
  138. }
  139. #pragma mark - Analytics
  140. - (void)sendAnalyticsEventWithSuccess:(BOOL)success {
  141. NSString *event = [NSString stringWithFormat:@"ios.%@.card.%@", self.apiClient.metadata.integrationString, success ? @"succeeded" : @"failed"];
  142. [self.apiClient sendAnalyticsEvent:event];
  143. }
  144. - (void)sendGraphQLAnalyticsEventWithSuccess:(BOOL)success {
  145. NSString *event = [NSString stringWithFormat:@"ios.card.graphql.tokenization.%@", success ? @"success" : @"failure"];
  146. [self.apiClient sendAnalyticsEvent:event];
  147. }
  148. - (void)sendUnionPayAnalyticsEvent:(BOOL)success {
  149. NSString *event = [NSString stringWithFormat:@"ios.%@.unionpay.nonce-%@", self.apiClient.metadata.integrationString, success ? @"received" : @"failed"];
  150. [self.apiClient sendAnalyticsEvent:event];
  151. }
  152. #pragma mark - Helpers
  153. + (NSDictionary *)validationErrorUserInfo:(NSDictionary *)userInfo {
  154. NSMutableDictionary *mutableUserInfo = [userInfo mutableCopy];
  155. BTJSON *jsonResponse = userInfo[BTHTTPJSONResponseBodyKey];
  156. if ([jsonResponse asDictionary]) {
  157. mutableUserInfo[BTCustomerInputBraintreeValidationErrorsKey] = [jsonResponse asDictionary];
  158. NSString *errorMessage = [jsonResponse[@"error"][@"message"] asString];
  159. if (errorMessage) {
  160. mutableUserInfo[NSLocalizedDescriptionKey] = errorMessage;
  161. }
  162. BTJSON *fieldError = [jsonResponse[@"fieldErrors"] asArray].firstObject;
  163. BTJSON *firstFieldError = [fieldError[@"fieldErrors"] asArray].firstObject;
  164. NSString *firstFieldErrorMessage = [firstFieldError[@"message"] asString];
  165. if (firstFieldErrorMessage) {
  166. mutableUserInfo[NSLocalizedFailureReasonErrorKey] = firstFieldErrorMessage;
  167. }
  168. }
  169. return [mutableUserInfo copy];
  170. }
  171. - (NSDictionary *)clientAPIParametersForCard:(BTCardRequest *)request options:(NSDictionary *)options {
  172. NSMutableDictionary *parameters = [NSMutableDictionary new];
  173. if (request.card.parameters) {
  174. NSMutableDictionary *mutableCardParameters = [request.card.parameters mutableCopy];
  175. if (request.enrollmentID) {
  176. // Convert the immutable options dictionary so to write to it without overwriting any existing options
  177. NSMutableDictionary *unionPayEnrollment = [NSMutableDictionary new];
  178. unionPayEnrollment[@"id"] = request.enrollmentID;
  179. if (request.smsCode) {
  180. unionPayEnrollment[@"sms_code"] = request.smsCode;
  181. }
  182. mutableCardParameters[@"options"] = [mutableCardParameters[@"options"] mutableCopy];
  183. mutableCardParameters[@"options"][@"union_pay_enrollment"] = unionPayEnrollment;
  184. }
  185. parameters[@"credit_card"] = [mutableCardParameters copy];
  186. }
  187. parameters[@"_meta"] = @{
  188. @"source" : self.apiClient.metadata.sourceString,
  189. @"integration" : self.apiClient.metadata.integrationString,
  190. @"sessionId" : self.apiClient.metadata.sessionID,
  191. };
  192. if (options) {
  193. parameters[@"options"] = options;
  194. }
  195. if (request.card.authenticationInsightRequested) {
  196. parameters[@"authenticationInsight"] = @YES;
  197. parameters[@"merchantAccountId"] = request.card.merchantAccountID;
  198. }
  199. return [parameters copy];
  200. }
  201. - (BOOL)isGraphQLEnabledForCardTokenization:(BTConfiguration *)configuration {
  202. NSArray *graphQLFeatures = [configuration.json[@"graphQL"][@"features"] asStringArray];
  203. return graphQLFeatures && [graphQLFeatures containsObject:BTCardClientGraphQLTokenizeFeature];
  204. }
  205. - (NSError *)constructCallbackErrorForErrorUserInfo:(NSDictionary *)errorUserInfo error:(NSError *)error {
  206. NSError *callbackError = error;
  207. BTJSON *errorCode = nil;
  208. BTJSON *errorResponse = [error.userInfo objectForKey:BTHTTPJSONResponseBodyKey];
  209. BTJSON *fieldErrors = [errorResponse[@"fieldErrors"] asArray].firstObject;
  210. errorCode = [fieldErrors[@"fieldErrors"] asArray].firstObject[@"code"];
  211. if (errorCode == nil) {
  212. BTJSON *errorResponse = [errorUserInfo objectForKey:BTHTTPJSONResponseBodyKey];
  213. errorCode = [errorResponse[@"errors"] asArray].firstObject[@"extensions"][@"legacyCode"];
  214. }
  215. // Gateway error code for card already exists
  216. if ([errorCode.asString isEqual: @"81724"]) {
  217. callbackError = [NSError errorWithDomain:BTCardClientErrorDomain
  218. code:BTCardClientErrorTypeCardAlreadyExists
  219. userInfo:[self.class validationErrorUserInfo:error.userInfo]];
  220. } else {
  221. callbackError = [NSError errorWithDomain:BTCardClientErrorDomain
  222. code:BTCardClientErrorTypeCustomerInputInvalid
  223. userInfo:[self.class validationErrorUserInfo:error.userInfo]];
  224. }
  225. return callbackError;
  226. }
  227. @end