BTAPIClient.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. #import "BTAPIClient_Internal.h"
  2. #import "BTAnalyticsService.h"
  3. #import "BTAnalyticsMetadata.h"
  4. #import "BTAPIHTTP.h"
  5. #import "BTGraphQLHTTP.h"
  6. #import "BTHTTP.h"
  7. #import "BTLogger_Internal.h"
  8. #import "BTPaymentMethodNonceParser.h"
  9. #if __has_include(<Braintree/BraintreeCore.h>)
  10. #import <Braintree/BTClientToken.h>
  11. #import <Braintree/BTConfiguration.h>
  12. #import <Braintree/BTJSON.h>
  13. #import <Braintree/BTPaymentMethodNonce.h>
  14. #else
  15. #import <BraintreeCore/BTClientToken.h>
  16. #import <BraintreeCore/BTConfiguration.h>
  17. #import <BraintreeCore/BTJSON.h>
  18. #import <BraintreeCore/BTPaymentMethodNonce.h>
  19. #endif
  20. NSString *const BTAPIClientErrorDomain = @"com.braintreepayments.BTAPIClientErrorDomain";
  21. @interface BTAPIClient ()
  22. @property (nonatomic, strong) dispatch_queue_t configurationQueue;
  23. @end
  24. @implementation BTAPIClient
  25. - (nullable instancetype)initWithAuthorization:(NSString *)authorization {
  26. return [self initWithAuthorization:authorization sendAnalyticsEvent:YES];
  27. }
  28. - (nullable instancetype)initWithAuthorization:(NSString *)authorization sendAnalyticsEvent:(BOOL)sendAnalyticsEvent {
  29. if(![authorization isKindOfClass:[NSString class]]) {
  30. NSString *reason = @"BTClient could not initialize because the provided authorization was invalid";
  31. [[BTLogger sharedLogger] error:reason];
  32. return nil;
  33. }
  34. if (self = [super init]) {
  35. BTAPIClientAuthorizationType authorizationType = [[self class] authorizationTypeForAuthorization:authorization];
  36. switch (authorizationType) {
  37. case BTAPIClientAuthorizationTypeTokenizationKey: {
  38. NSURL *baseURL = [BTAPIClient baseURLFromTokenizationKey:authorization];
  39. if (!baseURL) {
  40. NSString *reason = @"BTClient could not initialize because the provided tokenization key was invalid";
  41. [[BTLogger sharedLogger] error:reason];
  42. return nil;
  43. }
  44. _tokenizationKey = authorization;
  45. _configurationHTTP = [[BTHTTP alloc] initWithBaseURL:baseURL tokenizationKey:authorization];
  46. if (sendAnalyticsEvent) {
  47. [self queueAnalyticsEvent:@"ios.started.client-key"];
  48. }
  49. break;
  50. }
  51. case BTAPIClientAuthorizationTypeClientToken: {
  52. NSError *error;
  53. _clientToken = [[BTClientToken alloc] initWithClientToken:authorization error:&error];
  54. if (error) { [[BTLogger sharedLogger] error:[error localizedDescription]]; }
  55. if (!_clientToken) {
  56. [[BTLogger sharedLogger] error:@"BTClient could not initialize because the provided clientToken was invalid"];
  57. return nil;
  58. }
  59. _configurationHTTP = [[BTHTTP alloc] initWithClientToken:self.clientToken];
  60. if (sendAnalyticsEvent) {
  61. [self queueAnalyticsEvent:@"ios.started.client-token"];
  62. }
  63. break;
  64. }
  65. }
  66. _metadata = [[BTClientMetadata alloc] init];
  67. _configurationQueue = dispatch_queue_create("com.braintreepayments.BTAPIClient", DISPATCH_QUEUE_SERIAL);
  68. // BTHTTP's default NSURLSession does not cache responses, but we want the BTHTTP instance that fetches configuration to cache aggressively
  69. NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
  70. static NSURLCache *configurationCache;
  71. static dispatch_once_t onceToken;
  72. dispatch_once(&onceToken, ^{
  73. configurationCache = [[NSURLCache alloc] initWithMemoryCapacity:1 * 1024 * 1024 diskCapacity:0 diskPath:nil];
  74. });
  75. configuration.URLCache = configurationCache;
  76. // Use the caching logic defined in the protocol implementation, if any, for a particular URL load request.
  77. configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
  78. _configurationHTTP.session = [NSURLSession sessionWithConfiguration:configuration];
  79. // Kickoff the background request to fetch the config
  80. [self fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration * _Nullable configuration, __unused NSError * _Nullable error) {
  81. //noop
  82. }];
  83. }
  84. return self;
  85. }
  86. + (BTAPIClientAuthorizationType)authorizationTypeForAuthorization:(NSString *)authorization {
  87. NSRegularExpression *isTokenizationKeyRegExp = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9]+_[a-zA-Z0-9]+_[a-zA-Z0-9_]+$" options:0 error:NULL];
  88. NSTextCheckingResult *tokenizationKeyMatch = [isTokenizationKeyRegExp firstMatchInString:authorization options:0 range: NSMakeRange(0, authorization.length)];
  89. if (tokenizationKeyMatch) {
  90. return BTAPIClientAuthorizationTypeTokenizationKey;
  91. } else {
  92. return BTAPIClientAuthorizationTypeClientToken;
  93. }
  94. }
  95. // NEXT_MAJOR_VERSION: remove this unused method
  96. - (instancetype)copyWithSource:(BTClientMetadataSourceType)source
  97. integration:(BTClientMetadataIntegrationType)integration
  98. {
  99. BTAPIClient *copiedClient;
  100. if (self.clientToken) {
  101. copiedClient = [[[self class] alloc] initWithAuthorization:self.clientToken.originalValue sendAnalyticsEvent:NO];
  102. } else if (self.tokenizationKey) {
  103. copiedClient = [[[self class] alloc] initWithAuthorization:self.tokenizationKey sendAnalyticsEvent:NO];
  104. } else {
  105. NSAssert(NO, @"Cannot copy an API client that does not specify a client token or tokenization key");
  106. }
  107. if (copiedClient) {
  108. BTMutableClientMetadata *mutableMetadata = [self.metadata mutableCopy];
  109. mutableMetadata.source = source;
  110. mutableMetadata.integration = integration;
  111. copiedClient->_metadata = [mutableMetadata copy];
  112. }
  113. return copiedClient;
  114. }
  115. #pragma mark - Base URL
  116. /// Gets base URL from tokenization key
  117. ///
  118. /// @param tokenizationKey The tokenization key
  119. ///
  120. /// @return Base URL for environment, or `nil` if tokenization key is invalid
  121. + (NSURL *)baseURLFromTokenizationKey:(NSString *)tokenizationKey {
  122. NSRegularExpression *regExp = [NSRegularExpression regularExpressionWithPattern:@"([a-zA-Z0-9]+)_[a-zA-Z0-9]+_([a-zA-Z0-9_]+)" options:0 error:NULL];
  123. NSArray *results = [regExp matchesInString:tokenizationKey options:0 range:NSMakeRange(0, tokenizationKey.length)];
  124. if (results.count != 1 || [[results firstObject] numberOfRanges] != 3) {
  125. return nil;
  126. }
  127. NSString *environment = [tokenizationKey substringWithRange:[results[0] rangeAtIndex:1]];
  128. NSString *merchantID = [tokenizationKey substringWithRange:[results[0] rangeAtIndex:2]];
  129. NSURLComponents *components = [[NSURLComponents alloc] init];
  130. components.scheme = [BTAPIClient schemeForEnvironmentString:environment];
  131. NSString *host = [BTAPIClient hostForEnvironmentString:environment];
  132. NSArray <NSString *> *hostComponents = [host componentsSeparatedByString:@":"];
  133. components.host = hostComponents[0];
  134. if (hostComponents.count > 1) {
  135. NSString *portString = hostComponents[1];
  136. components.port = @(portString.integerValue);
  137. }
  138. components.path = [BTAPIClient clientApiBasePathForMerchantID:merchantID];
  139. if (!components.host || !components.path) {
  140. return nil;
  141. }
  142. return components.URL;
  143. }
  144. + (NSString *)schemeForEnvironmentString:(NSString *)environment {
  145. if ([[environment lowercaseString] isEqualToString:@"development"]) {
  146. return @"http";
  147. }
  148. return @"https";
  149. }
  150. + (NSString *)hostForEnvironmentString:(NSString *)environment {
  151. if ([[environment lowercaseString] isEqualToString:@"sandbox"]) {
  152. return @"api.sandbox.braintreegateway.com";
  153. } else if ([[environment lowercaseString] isEqualToString:@"production"]) {
  154. return @"api.braintreegateway.com:443";
  155. } else if ([[environment lowercaseString] isEqualToString:@"development"]) {
  156. return @"localhost:3000";
  157. } else {
  158. return nil;
  159. }
  160. }
  161. + (NSURL *)graphQLURLForEnvironment:(NSString *)environment {
  162. NSURLComponents *components = [[NSURLComponents alloc] init];
  163. components.scheme = [BTAPIClient schemeForEnvironmentString:environment];
  164. NSString *host = [BTAPIClient graphQLHostForEnvironmentString:environment];
  165. NSArray <NSString *> *hostComponents = [host componentsSeparatedByString:@":"];
  166. if (hostComponents.count == 0) {
  167. return nil;
  168. }
  169. components.host = hostComponents[0];
  170. if (hostComponents.count > 1) {
  171. NSString *portString = hostComponents[1];
  172. components.port = @(portString.integerValue);
  173. }
  174. components.path = @"/graphql";
  175. return components.URL;
  176. }
  177. + (NSString *)graphQLHostForEnvironmentString:(NSString *)environment {
  178. if ([[environment lowercaseString] isEqualToString:@"sandbox"]) {
  179. return @"payments.sandbox.braintree-api.com";
  180. } else if ([[environment lowercaseString] isEqualToString:@"development"]) {
  181. return @"localhost:8080";
  182. } else {
  183. return @"payments.braintree-api.com";
  184. }
  185. }
  186. + (NSString *)clientApiBasePathForMerchantID:(NSString *)merchantID {
  187. if (merchantID.length == 0) {
  188. return nil;
  189. }
  190. return [NSString stringWithFormat:@"/merchants/%@/client_api", merchantID];
  191. }
  192. # pragma mark - Payment Methods
  193. // NEXT_MAJOR_VERSION - move fetchPaymentMethodNonces methods to Drop-in for compatibility with Android
  194. // This will also allow us to remove BTPaymentMethodNonceParser
  195. - (void)fetchPaymentMethodNonces:(void (^)(NSArray <BTPaymentMethodNonce *> *, NSError *))completion {
  196. [self fetchPaymentMethodNonces:NO completion:completion];
  197. }
  198. - (void)fetchPaymentMethodNonces:(BOOL)defaultFirst completion:(void (^)(NSArray <BTPaymentMethodNonce *> *, NSError *))completion {
  199. if (!self.clientToken) {
  200. NSError *error = [NSError errorWithDomain:BTAPIClientErrorDomain code:BTAPIClientErrorTypeNotAuthorized userInfo:@{ NSLocalizedDescriptionKey : @"Cannot fetch payment method nonces with a tokenization key", NSLocalizedRecoverySuggestionErrorKey : @"This endpoint requires a client token for authorization"}];
  201. if (completion) {
  202. completion(nil, error);
  203. }
  204. return;
  205. }
  206. NSString *defaultFirstValue = defaultFirst ? @"true" : @"false";
  207. [self GET:@"v1/payment_methods"
  208. parameters:@{@"default_first": defaultFirstValue,
  209. @"session_id": self.metadata.sessionID}
  210. completion:^(BTJSON * _Nullable body, __unused NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
  211. dispatch_async(dispatch_get_main_queue(), ^{
  212. if (completion) {
  213. if (error) {
  214. completion(nil, error);
  215. } else {
  216. NSMutableArray *paymentMethodNonces = [NSMutableArray array];
  217. for (BTJSON *paymentInfo in [body[@"paymentMethods"] asArray]) {
  218. BTPaymentMethodNonce *paymentMethodNonce = [[BTPaymentMethodNonceParser sharedParser] parseJSON:paymentInfo withParsingBlockForType:[paymentInfo[@"type"] asString]];
  219. if (paymentMethodNonce) {
  220. [paymentMethodNonces addObject:paymentMethodNonce];
  221. }
  222. }
  223. completion(paymentMethodNonces, nil);
  224. }
  225. }
  226. });
  227. }];
  228. }
  229. #pragma mark - Remote Configuration
  230. - (void)fetchOrReturnRemoteConfiguration:(void (^)(BTConfiguration *, NSError *))completionBlock {
  231. // Fetches or returns the configuration and caches the response in the GET BTHTTP call if successful
  232. //
  233. // Rules:
  234. // - If cachedConfiguration is present, return it without a request
  235. // - If cachedConfiguration is not present, fetch it and cache the successful response
  236. // - If fetching fails, return error
  237. NSString *configPath = @"v1/configuration"; // Default for tokenizationKey
  238. if (self.clientToken) {
  239. configPath = [self.clientToken.configURL absoluteString];
  240. }
  241. [self.configurationHTTP GET:configPath parameters:@{ @"configVersion": @"3" } shouldCache:YES completion:^(BTJSON * _Nullable body, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
  242. NSError *fetchError;
  243. BTConfiguration *configuration;
  244. if (error) {
  245. fetchError = error;
  246. } else if (response.statusCode != 200) {
  247. NSError *configurationDomainError =
  248. [NSError errorWithDomain:BTAPIClientErrorDomain
  249. code:BTAPIClientErrorTypeConfigurationUnavailable
  250. userInfo:@{
  251. NSLocalizedFailureReasonErrorKey: @"Unable to fetch remote configuration from Braintree API at this time."
  252. }];
  253. fetchError = configurationDomainError;
  254. } else {
  255. configuration = [[BTConfiguration alloc] initWithJSON:body];
  256. if (!self.braintreeAPI) {
  257. NSURL *apiURL = [configuration.json[@"braintreeApi"][@"url"] asURL];
  258. NSString *accessToken = [configuration.json[@"braintreeApi"][@"accessToken"] asString];
  259. self.braintreeAPI = [[BTAPIHTTP alloc] initWithBaseURL:apiURL accessToken:accessToken];
  260. }
  261. if (!self.http) {
  262. NSURL *baseURL = [configuration.json[@"clientApiUrl"] asURL];
  263. if (self.clientToken) {
  264. self.http = [[BTHTTP alloc] initWithBaseURL:baseURL authorizationFingerprint:self.clientToken.authorizationFingerprint];
  265. } else if (self.tokenizationKey) {
  266. self.http = [[BTHTTP alloc] initWithBaseURL:baseURL tokenizationKey:self.tokenizationKey];
  267. }
  268. }
  269. if (!self.graphQL) {
  270. NSURL *graphQLBaseURL = [BTAPIClient graphQLURLForEnvironment:configuration.environment];
  271. if (self.clientToken) {
  272. self.graphQL = [[BTGraphQLHTTP alloc] initWithBaseURL:graphQLBaseURL authorizationFingerprint:self.clientToken.authorizationFingerprint];
  273. } else if (self.tokenizationKey) {
  274. self.graphQL = [[BTGraphQLHTTP alloc] initWithBaseURL:graphQLBaseURL tokenizationKey:self.tokenizationKey];
  275. }
  276. }
  277. }
  278. completionBlock(configuration, fetchError);
  279. }];
  280. }
  281. #pragma mark - Analytics
  282. /// By default, the `BTAnalyticsService` instance is static/shared so that only one queue of events exists.
  283. /// The "singleton" is managed here because the analytics service depends on `BTAPIClient`.
  284. - (BTAnalyticsService *)analyticsService {
  285. static BTAnalyticsService *analyticsService;
  286. static dispatch_once_t onceToken;
  287. dispatch_once(&onceToken, ^{
  288. analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:self];
  289. analyticsService.flushThreshold = 5;
  290. });
  291. // The analytics service may be overridden by unit tests. In that case, return the ivar and not the singleton
  292. if (_analyticsService) return _analyticsService;
  293. return analyticsService;
  294. }
  295. - (void)sendAnalyticsEvent:(NSString *)eventKind {
  296. [self.analyticsService sendAnalyticsEvent:eventKind completion:nil];
  297. }
  298. - (void)queueAnalyticsEvent:(NSString *)eventKind {
  299. [self.analyticsService sendAnalyticsEvent:eventKind];
  300. }
  301. - (NSDictionary *)metaParameters {
  302. NSMutableDictionary *metaParameters = [NSMutableDictionary dictionaryWithDictionary:self.metadata.parameters];
  303. [metaParameters addEntriesFromDictionary:[BTAnalyticsMetadata metadata]];
  304. return [metaParameters copy];
  305. }
  306. - (NSDictionary *)graphQLMetadata {
  307. return self.metadata.parameters;
  308. }
  309. - (NSDictionary *)metaParametersWithParameters:(NSDictionary *)parameters forHTTPType:(BTAPIClientHTTPType)httpType {
  310. if (httpType == BTAPIClientHTTPTypeBraintreeAPI) {
  311. return parameters;
  312. }
  313. NSMutableDictionary *mutableParameters = [NSMutableDictionary dictionaryWithDictionary:parameters];
  314. if (httpType == BTAPIClientHTTPTypeGraphQLAPI) {
  315. mutableParameters[@"clientSdkMetadata"] = [self graphQLMetadata];
  316. } else if (httpType == BTAPIClientHTTPTypeGateway) {
  317. mutableParameters[@"_meta"] = [self metaParameters];
  318. }
  319. return [mutableParameters copy];
  320. }
  321. #pragma mark - HTTP Operations
  322. - (void)GET:(NSString *)endpoint parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  323. [self GET:endpoint parameters:parameters httpType:BTAPIClientHTTPTypeGateway completion:completionBlock];
  324. }
  325. - (void)POST:(NSString *)endpoint parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  326. [self POST:endpoint parameters:parameters httpType:BTAPIClientHTTPTypeGateway completion:completionBlock];
  327. }
  328. - (void)GET:(NSString *)endpoint parameters:(NSDictionary *)parameters httpType:(BTAPIClientHTTPType)httpType completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  329. [self fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration * _Nullable configuration, __unused NSError * _Nullable error) {
  330. if (error != nil) {
  331. completionBlock(nil, nil, error);
  332. return;
  333. }
  334. [[self httpForType:httpType] GET:endpoint parameters:parameters completion:completionBlock];
  335. }];
  336. }
  337. - (void)POST:(NSString *)endpoint parameters:(NSDictionary *)parameters httpType:(BTAPIClientHTTPType)httpType completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  338. [self fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration * _Nullable configuration, __unused NSError * _Nullable error) {
  339. if (error != nil) {
  340. completionBlock(nil, nil, error);
  341. return;
  342. }
  343. NSDictionary *postParameters = [self metaParametersWithParameters:parameters forHTTPType:httpType];
  344. [[self httpForType:httpType] POST:endpoint parameters:postParameters completion:completionBlock];
  345. }];
  346. }
  347. - (BTHTTP *)httpForType:(BTAPIClientHTTPType)httpType {
  348. if (httpType == BTAPIClientHTTPTypeBraintreeAPI) {
  349. return self.braintreeAPI;
  350. } else if (httpType == BTAPIClientHTTPTypeGraphQLAPI) {
  351. return self.graphQL;
  352. }
  353. return self.http;
  354. }
  355. - (instancetype)init NS_UNAVAILABLE
  356. {
  357. return nil;
  358. }
  359. - (void)dealloc
  360. {
  361. if (self.http && self.http.session) {
  362. [self.http.session finishTasksAndInvalidate];
  363. }
  364. if (self.braintreeAPI && self.braintreeAPI.session) {
  365. [self.braintreeAPI.session finishTasksAndInvalidate];
  366. }
  367. if (self.graphQL && self.graphQL.session) {
  368. [self.graphQL.session finishTasksAndInvalidate];
  369. }
  370. [self.configurationHTTP.session.configuration.URLCache removeAllCachedResponses];
  371. }
  372. @end