BTCardClient.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. #import "BTErrors.h"
  2. #import "BTCardClient_Internal.h"
  3. #import "BTCardNonce_Internal.h"
  4. #import "BTCardRequest.h"
  5. #import "BTConfiguration+Card.h"
  6. #import "BTClientMetadata.h"
  7. #import "BTHTTP.h"
  8. #import "BTJSON.h"
  9. #import "BTPaymentMethodNonceParser.h"
  10. #import "BTTokenizationService.h"
  11. #if __has_include("BraintreeCore.h")
  12. #import "BTAPIClient_Internal.h"
  13. #import "BTCard_Internal.h"
  14. #else
  15. #import <BraintreeCore/BTAPIClient_Internal.h>
  16. #import <BraintreeCore/BTCard_Internal.h>
  17. #endif
  18. NSString *const BTCardClientErrorDomain = @"com.braintreepayments.BTCardClientErrorDomain";
  19. NSString *const BTCardClientGraphQLTokenizeFeature = @"tokenize_credit_cards";
  20. @interface BTCardClient ()
  21. @end
  22. @implementation BTCardClient
  23. static Class PayPalDataCollectorClass;
  24. static NSString *PayPalDataCollectorClassString = @"PPDataCollector";
  25. + (void)load {
  26. if (self == [BTCardClient class]) {
  27. [[BTTokenizationService sharedService] registerType:@"Card" withTokenizationBlock:^(BTAPIClient *apiClient, NSDictionary *options, void (^completionBlock)(BTPaymentMethodNonce *paymentMethodNonce, NSError *error)) {
  28. BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient];
  29. [client tokenizeCard:[[BTCard alloc] initWithParameters:options] completion:completionBlock];
  30. }];
  31. [[BTPaymentMethodNonceParser sharedParser] registerType:@"CreditCard" withParsingBlock:^BTPaymentMethodNonce * _Nullable(BTJSON * _Nonnull creditCard) {
  32. return [BTCardNonce cardNonceWithJSON:creditCard];
  33. }];
  34. }
  35. }
  36. - (instancetype)initWithAPIClient:(BTAPIClient *)apiClient {
  37. if (!apiClient) {
  38. return nil;
  39. }
  40. if (self = [super init]) {
  41. self.apiClient = apiClient;
  42. }
  43. return self;
  44. }
  45. - (instancetype)init {
  46. return nil;
  47. }
  48. - (void)tokenizeCard:(BTCard *)card completion:(void (^)(BTCardNonce *tokenizedCard, NSError *error))completion {
  49. BTCardRequest *request = [[BTCardRequest alloc] initWithCard:card];
  50. [self tokenizeCard:request options:nil completion:completion];
  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. NSHTTPURLResponse *response = error.userInfo[BTHTTPURLResponseKey];
  83. NSError *callbackError = error;
  84. if (response.statusCode == 422) {
  85. callbackError = [NSError errorWithDomain:BTCardClientErrorDomain
  86. code:BTCardClientErrorTypeCustomerInputInvalid
  87. userInfo:[self.class validationErrorUserInfo:error.userInfo]];
  88. }
  89. [self sendGraphQLAnalyticsEventWithSuccess:NO];
  90. completionBlock(nil, callbackError);
  91. return;
  92. }
  93. BTJSON *cardJSON = body[@"data"][@"tokenizeCreditCard"];
  94. [self sendGraphQLAnalyticsEventWithSuccess:YES];
  95. BTCardNonce *cardNonce = [BTCardNonce cardNonceWithGraphQLJSON:cardJSON];
  96. if (cardNonce && [self isPayPalDataCollectorAvailable] && [configuration collectFraudData]) {
  97. [self collectRiskData:cardNonce.nonce configuration:configuration];
  98. }
  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. NSHTTPURLResponse *response = error.userInfo[BTHTTPURLResponseKey];
  109. NSError *callbackError = error;
  110. if (response.statusCode == 422) {
  111. callbackError = [NSError errorWithDomain:BTCardClientErrorDomain
  112. code:BTCardClientErrorTypeCustomerInputInvalid
  113. userInfo:[self.class validationErrorUserInfo:error.userInfo]];
  114. }
  115. if (request.enrollmentID) {
  116. [self sendUnionPayAnalyticsEvent:NO];
  117. } else {
  118. [self sendAnalyticsEventWithSuccess:NO];
  119. }
  120. completionBlock(nil, callbackError);
  121. return;
  122. }
  123. BTJSON *cardJSON = body[@"creditCards"][0];
  124. if (request.enrollmentID) {
  125. [self sendUnionPayAnalyticsEvent:!cardJSON.isError];
  126. } else {
  127. [self sendAnalyticsEventWithSuccess:!cardJSON.isError];
  128. }
  129. // cardNonceWithJSON returns nil when cardJSON is nil, cardJSON.asError is nil when cardJSON is non-nil
  130. BTCardNonce *cardNonce = [BTCardNonce cardNonceWithJSON:cardJSON];
  131. if (cardNonce && [self isPayPalDataCollectorAvailable] && [configuration collectFraudData]) {
  132. [self collectRiskData:cardNonce.nonce configuration:configuration];
  133. }
  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. BTJSON *fieldError = [[jsonResponse[@"fieldErrors"] asArray] firstObject];
  159. NSString *errorMessage = [jsonResponse[@"error"][@"message"] asString];
  160. if (errorMessage) {
  161. mutableUserInfo[NSLocalizedDescriptionKey] = errorMessage;
  162. }
  163. NSString *firstFieldErrorMessage = [fieldError[@"fieldErrors"] firstObject][@"message"];
  164. if (firstFieldErrorMessage) {
  165. mutableUserInfo[NSLocalizedFailureReasonErrorKey] = firstFieldErrorMessage;
  166. }
  167. }
  168. return [mutableUserInfo copy];
  169. }
  170. - (NSDictionary *)clientAPIParametersForCard:(BTCardRequest *)request options:(NSDictionary *)options {
  171. NSMutableDictionary *parameters = [NSMutableDictionary new];
  172. if (request.card.parameters) {
  173. NSMutableDictionary *mutableCardParameters = [request.card.parameters mutableCopy];
  174. if (request.enrollmentID) {
  175. // Convert the immutable options dictionary so to write to it without overwriting any existing options
  176. NSMutableDictionary *unionPayEnrollment = [NSMutableDictionary new];
  177. unionPayEnrollment[@"id"] = request.enrollmentID;
  178. if (request.smsCode) {
  179. unionPayEnrollment[@"sms_code"] = request.smsCode;
  180. }
  181. mutableCardParameters[@"options"] = [mutableCardParameters[@"options"] mutableCopy];
  182. mutableCardParameters[@"options"][@"union_pay_enrollment"] = unionPayEnrollment;
  183. }
  184. parameters[@"credit_card"] = [mutableCardParameters copy];
  185. }
  186. parameters[@"_meta"] = @{
  187. @"source" : self.apiClient.metadata.sourceString,
  188. @"integration" : self.apiClient.metadata.integrationString,
  189. @"sessionId" : self.apiClient.metadata.sessionId,
  190. };
  191. if (options) {
  192. parameters[@"options"] = options;
  193. }
  194. if (request.card.authenticationInsightRequested) {
  195. parameters[@"authenticationInsight"] = @YES;
  196. parameters[@"merchantAccountId"] = request.card.merchantAccountId;
  197. }
  198. return [parameters copy];
  199. }
  200. - (BOOL)isGraphQLEnabledForCardTokenization:(BTConfiguration *)configuration {
  201. NSArray *graphQLFeatures = [configuration.json[@"graphQL"][@"features"] asArray];
  202. return graphQLFeatures && [graphQLFeatures containsObject:BTCardClientGraphQLTokenizeFeature];
  203. }
  204. + (void)setPayPalDataCollectorClassString:(NSString *)payPalDataCollectorClassString {
  205. PayPalDataCollectorClassString = payPalDataCollectorClassString;
  206. }
  207. + (void)setPayPalDataCollectorClass:(Class)payPalDataCollectorClass {
  208. PayPalDataCollectorClass = payPalDataCollectorClass;
  209. }
  210. - (BOOL)isPayPalDataCollectorAvailable {
  211. Class kPPDataCollector = NSClassFromString(PayPalDataCollectorClassString);
  212. SEL aSelector = NSSelectorFromString(@"generateClientMetadataIDWithoutBeacon:data:");
  213. return kPPDataCollector && [kPPDataCollector respondsToSelector:aSelector];
  214. }
  215. - (void)collectRiskData:(NSString *)correlationId configuration:(BTConfiguration *)configuration {
  216. // Trim to 32 chars to ensure compatibility with PPDataCollector
  217. NSString *trimmedCorrelationId = [correlationId copy];
  218. if (trimmedCorrelationId && [trimmedCorrelationId length] > 32) {
  219. trimmedCorrelationId = [trimmedCorrelationId substringToIndex:32];
  220. }
  221. NSMutableDictionary *data = [@{
  222. @"mid":[configuration.json[@"merchantId"] asString],
  223. @"rda_tenant": @"bt_card"
  224. } mutableCopy];
  225. if (self.apiClient.clientToken != nil) {
  226. NSString *authorizationFingerprint = self.apiClient.clientToken.authorizationFingerprint;
  227. NSArray *authorizationComponents = [authorizationFingerprint componentsSeparatedByString:@"&"];
  228. for (NSString *component in authorizationComponents) {
  229. if ([component hasPrefix:@"customer_id="]) {
  230. NSArray *customerIdComponents = [component componentsSeparatedByString:@"="];
  231. if ([customerIdComponents count] > 1) {
  232. data[@"cid"] = [customerIdComponents lastObject];
  233. }
  234. }
  235. }
  236. }
  237. Class kPPDataCollector = [self getPPDataCollectorClass];
  238. SEL aSelector = NSSelectorFromString(@"generateClientMetadataIDWithoutBeacon:data:");
  239. if(kPPDataCollector != nil && [kPPDataCollector respondsToSelector:aSelector]) {
  240. NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[kPPDataCollector methodSignatureForSelector:aSelector]];
  241. [inv setSelector:aSelector];
  242. [inv setTarget:kPPDataCollector];
  243. [inv setArgument:&(trimmedCorrelationId) atIndex:2];
  244. [inv setArgument:&(data) atIndex:3];
  245. [inv invoke];
  246. }
  247. }
  248. - (Class)getPPDataCollectorClass {
  249. if (PayPalDataCollectorClass != nil) {
  250. return PayPalDataCollectorClass;
  251. }
  252. return NSClassFromString(PayPalDataCollectorClassString);
  253. }
  254. @end