| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- #import "BTAPIClient_Internal.h"
- #import "BTAnalyticsService.h"
- #import "BTAnalyticsMetadata.h"
- #import "BTAPIHTTP.h"
- #import "BTGraphQLHTTP.h"
- #import "BTHTTP.h"
- #import "BTLogger_Internal.h"
- #import "BTPaymentMethodNonceParser.h"
- #if __has_include(<Braintree/BraintreeCore.h>)
- #import <Braintree/BTClientToken.h>
- #import <Braintree/BTConfiguration.h>
- #import <Braintree/BTJSON.h>
- #import <Braintree/BTPaymentMethodNonce.h>
- #else
- #import <BraintreeCore/BTClientToken.h>
- #import <BraintreeCore/BTConfiguration.h>
- #import <BraintreeCore/BTJSON.h>
- #import <BraintreeCore/BTPaymentMethodNonce.h>
- #endif
- NSString *const BTAPIClientErrorDomain = @"com.braintreepayments.BTAPIClientErrorDomain";
- @interface BTAPIClient ()
- @property (nonatomic, strong) dispatch_queue_t configurationQueue;
- @end
- @implementation BTAPIClient
- - (nullable instancetype)initWithAuthorization:(NSString *)authorization {
- return [self initWithAuthorization:authorization sendAnalyticsEvent:YES];
- }
- - (nullable instancetype)initWithAuthorization:(NSString *)authorization sendAnalyticsEvent:(BOOL)sendAnalyticsEvent {
- if(![authorization isKindOfClass:[NSString class]]) {
- NSString *reason = @"BTClient could not initialize because the provided authorization was invalid";
- [[BTLogger sharedLogger] error:reason];
- return nil;
- }
- if (self = [super init]) {
- BTAPIClientAuthorizationType authorizationType = [[self class] authorizationTypeForAuthorization:authorization];
- switch (authorizationType) {
- case BTAPIClientAuthorizationTypeTokenizationKey: {
- NSURL *baseURL = [BTAPIClient baseURLFromTokenizationKey:authorization];
- if (!baseURL) {
- NSString *reason = @"BTClient could not initialize because the provided tokenization key was invalid";
- [[BTLogger sharedLogger] error:reason];
- return nil;
- }
- _tokenizationKey = authorization;
- _configurationHTTP = [[BTHTTP alloc] initWithBaseURL:baseURL tokenizationKey:authorization];
- if (sendAnalyticsEvent) {
- [self queueAnalyticsEvent:@"ios.started.client-key"];
- }
- break;
- }
- case BTAPIClientAuthorizationTypeClientToken: {
- NSError *error;
- _clientToken = [[BTClientToken alloc] initWithClientToken:authorization error:&error];
- if (error) { [[BTLogger sharedLogger] error:[error localizedDescription]]; }
- if (!_clientToken) {
- [[BTLogger sharedLogger] error:@"BTClient could not initialize because the provided clientToken was invalid"];
- return nil;
- }
- _configurationHTTP = [[BTHTTP alloc] initWithClientToken:self.clientToken];
- if (sendAnalyticsEvent) {
- [self queueAnalyticsEvent:@"ios.started.client-token"];
- }
- break;
- }
- }
- _metadata = [[BTClientMetadata alloc] init];
- _configurationQueue = dispatch_queue_create("com.braintreepayments.BTAPIClient", DISPATCH_QUEUE_SERIAL);
- // BTHTTP's default NSURLSession does not cache responses, but we want the BTHTTP instance that fetches configuration to cache aggressively
- NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
- static NSURLCache *configurationCache;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- configurationCache = [[NSURLCache alloc] initWithMemoryCapacity:1 * 1024 * 1024 diskCapacity:0 diskPath:nil];
- });
- configuration.URLCache = configurationCache;
- // Use the caching logic defined in the protocol implementation, if any, for a particular URL load request.
- configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
- _configurationHTTP.session = [NSURLSession sessionWithConfiguration:configuration];
- // Kickoff the background request to fetch the config
- [self fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration * _Nullable configuration, __unused NSError * _Nullable error) {
- //noop
- }];
- }
- return self;
- }
- + (BTAPIClientAuthorizationType)authorizationTypeForAuthorization:(NSString *)authorization {
- NSRegularExpression *isTokenizationKeyRegExp = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9]+_[a-zA-Z0-9]+_[a-zA-Z0-9_]+$" options:0 error:NULL];
- NSTextCheckingResult *tokenizationKeyMatch = [isTokenizationKeyRegExp firstMatchInString:authorization options:0 range: NSMakeRange(0, authorization.length)];
-
- if (tokenizationKeyMatch) {
- return BTAPIClientAuthorizationTypeTokenizationKey;
- } else {
- return BTAPIClientAuthorizationTypeClientToken;
- }
- }
- // NEXT_MAJOR_VERSION: remove this unused method
- - (instancetype)copyWithSource:(BTClientMetadataSourceType)source
- integration:(BTClientMetadataIntegrationType)integration
- {
- BTAPIClient *copiedClient;
- if (self.clientToken) {
- copiedClient = [[[self class] alloc] initWithAuthorization:self.clientToken.originalValue sendAnalyticsEvent:NO];
- } else if (self.tokenizationKey) {
- copiedClient = [[[self class] alloc] initWithAuthorization:self.tokenizationKey sendAnalyticsEvent:NO];
- } else {
- NSAssert(NO, @"Cannot copy an API client that does not specify a client token or tokenization key");
- }
- if (copiedClient) {
- BTMutableClientMetadata *mutableMetadata = [self.metadata mutableCopy];
- mutableMetadata.source = source;
- mutableMetadata.integration = integration;
- copiedClient->_metadata = [mutableMetadata copy];
- }
- return copiedClient;
- }
- #pragma mark - Base URL
- /// Gets base URL from tokenization key
- ///
- /// @param tokenizationKey The tokenization key
- ///
- /// @return Base URL for environment, or `nil` if tokenization key is invalid
- + (NSURL *)baseURLFromTokenizationKey:(NSString *)tokenizationKey {
- NSRegularExpression *regExp = [NSRegularExpression regularExpressionWithPattern:@"([a-zA-Z0-9]+)_[a-zA-Z0-9]+_([a-zA-Z0-9_]+)" options:0 error:NULL];
- NSArray *results = [regExp matchesInString:tokenizationKey options:0 range:NSMakeRange(0, tokenizationKey.length)];
- if (results.count != 1 || [[results firstObject] numberOfRanges] != 3) {
- return nil;
- }
- NSString *environment = [tokenizationKey substringWithRange:[results[0] rangeAtIndex:1]];
- NSString *merchantID = [tokenizationKey substringWithRange:[results[0] rangeAtIndex:2]];
- NSURLComponents *components = [[NSURLComponents alloc] init];
- components.scheme = [BTAPIClient schemeForEnvironmentString:environment];
- NSString *host = [BTAPIClient hostForEnvironmentString:environment];
- NSArray <NSString *> *hostComponents = [host componentsSeparatedByString:@":"];
- components.host = hostComponents[0];
- if (hostComponents.count > 1) {
- NSString *portString = hostComponents[1];
- components.port = @(portString.integerValue);
- }
- components.path = [BTAPIClient clientApiBasePathForMerchantID:merchantID];
- if (!components.host || !components.path) {
- return nil;
- }
- return components.URL;
- }
- + (NSString *)schemeForEnvironmentString:(NSString *)environment {
- if ([[environment lowercaseString] isEqualToString:@"development"]) {
- return @"http";
- }
- return @"https";
- }
- + (NSString *)hostForEnvironmentString:(NSString *)environment {
- if ([[environment lowercaseString] isEqualToString:@"sandbox"]) {
- return @"api.sandbox.braintreegateway.com";
- } else if ([[environment lowercaseString] isEqualToString:@"production"]) {
- return @"api.braintreegateway.com:443";
- } else if ([[environment lowercaseString] isEqualToString:@"development"]) {
- return @"localhost:3000";
- } else {
- return nil;
- }
- }
- + (NSURL *)graphQLURLForEnvironment:(NSString *)environment {
- NSURLComponents *components = [[NSURLComponents alloc] init];
- components.scheme = [BTAPIClient schemeForEnvironmentString:environment];
- NSString *host = [BTAPIClient graphQLHostForEnvironmentString:environment];
- NSArray <NSString *> *hostComponents = [host componentsSeparatedByString:@":"];
- if (hostComponents.count == 0) {
- return nil;
- }
- components.host = hostComponents[0];
- if (hostComponents.count > 1) {
- NSString *portString = hostComponents[1];
- components.port = @(portString.integerValue);
- }
- components.path = @"/graphql";
- return components.URL;
- }
- + (NSString *)graphQLHostForEnvironmentString:(NSString *)environment {
- if ([[environment lowercaseString] isEqualToString:@"sandbox"]) {
- return @"payments.sandbox.braintree-api.com";
- } else if ([[environment lowercaseString] isEqualToString:@"development"]) {
- return @"localhost:8080";
- } else {
- return @"payments.braintree-api.com";
- }
- }
- + (NSString *)clientApiBasePathForMerchantID:(NSString *)merchantID {
- if (merchantID.length == 0) {
- return nil;
- }
- return [NSString stringWithFormat:@"/merchants/%@/client_api", merchantID];
- }
- # pragma mark - Payment Methods
- // NEXT_MAJOR_VERSION - move fetchPaymentMethodNonces methods to Drop-in for compatibility with Android
- // This will also allow us to remove BTPaymentMethodNonceParser
- - (void)fetchPaymentMethodNonces:(void (^)(NSArray <BTPaymentMethodNonce *> *, NSError *))completion {
- [self fetchPaymentMethodNonces:NO completion:completion];
- }
- - (void)fetchPaymentMethodNonces:(BOOL)defaultFirst completion:(void (^)(NSArray <BTPaymentMethodNonce *> *, NSError *))completion {
- if (!self.clientToken) {
- 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"}];
- if (completion) {
- completion(nil, error);
- }
- return;
- }
- NSString *defaultFirstValue = defaultFirst ? @"true" : @"false";
- [self GET:@"v1/payment_methods"
- parameters:@{@"default_first": defaultFirstValue,
- @"session_id": self.metadata.sessionID}
- completion:^(BTJSON * _Nullable body, __unused NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
- dispatch_async(dispatch_get_main_queue(), ^{
- if (completion) {
- if (error) {
- completion(nil, error);
- } else {
- NSMutableArray *paymentMethodNonces = [NSMutableArray array];
- for (BTJSON *paymentInfo in [body[@"paymentMethods"] asArray]) {
- BTPaymentMethodNonce *paymentMethodNonce = [[BTPaymentMethodNonceParser sharedParser] parseJSON:paymentInfo withParsingBlockForType:[paymentInfo[@"type"] asString]];
- if (paymentMethodNonce) {
- [paymentMethodNonces addObject:paymentMethodNonce];
- }
- }
- completion(paymentMethodNonces, nil);
- }
- }
- });
- }];
- }
- #pragma mark - Remote Configuration
- - (void)fetchOrReturnRemoteConfiguration:(void (^)(BTConfiguration *, NSError *))completionBlock {
- // Fetches or returns the configuration and caches the response in the GET BTHTTP call if successful
- //
- // Rules:
- // - If cachedConfiguration is present, return it without a request
- // - If cachedConfiguration is not present, fetch it and cache the successful response
- // - If fetching fails, return error
- NSString *configPath = @"v1/configuration"; // Default for tokenizationKey
- if (self.clientToken) {
- configPath = [self.clientToken.configURL absoluteString];
- }
- [self.configurationHTTP GET:configPath parameters:@{ @"configVersion": @"3" } shouldCache:YES completion:^(BTJSON * _Nullable body, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
- NSError *fetchError;
- BTConfiguration *configuration;
- if (error) {
- fetchError = error;
- } else if (response.statusCode != 200) {
- NSError *configurationDomainError =
- [NSError errorWithDomain:BTAPIClientErrorDomain
- code:BTAPIClientErrorTypeConfigurationUnavailable
- userInfo:@{
- NSLocalizedFailureReasonErrorKey: @"Unable to fetch remote configuration from Braintree API at this time."
- }];
- fetchError = configurationDomainError;
- } else {
- configuration = [[BTConfiguration alloc] initWithJSON:body];
- if (!self.braintreeAPI) {
- NSURL *apiURL = [configuration.json[@"braintreeApi"][@"url"] asURL];
- NSString *accessToken = [configuration.json[@"braintreeApi"][@"accessToken"] asString];
- self.braintreeAPI = [[BTAPIHTTP alloc] initWithBaseURL:apiURL accessToken:accessToken];
- }
- if (!self.http) {
- NSURL *baseURL = [configuration.json[@"clientApiUrl"] asURL];
- if (self.clientToken) {
- self.http = [[BTHTTP alloc] initWithBaseURL:baseURL authorizationFingerprint:self.clientToken.authorizationFingerprint];
- } else if (self.tokenizationKey) {
- self.http = [[BTHTTP alloc] initWithBaseURL:baseURL tokenizationKey:self.tokenizationKey];
- }
- }
- if (!self.graphQL) {
- NSURL *graphQLBaseURL = [BTAPIClient graphQLURLForEnvironment:configuration.environment];
- if (self.clientToken) {
- self.graphQL = [[BTGraphQLHTTP alloc] initWithBaseURL:graphQLBaseURL authorizationFingerprint:self.clientToken.authorizationFingerprint];
- } else if (self.tokenizationKey) {
- self.graphQL = [[BTGraphQLHTTP alloc] initWithBaseURL:graphQLBaseURL tokenizationKey:self.tokenizationKey];
- }
- }
- }
- completionBlock(configuration, fetchError);
- }];
- }
- #pragma mark - Analytics
- /// By default, the `BTAnalyticsService` instance is static/shared so that only one queue of events exists.
- /// The "singleton" is managed here because the analytics service depends on `BTAPIClient`.
- - (BTAnalyticsService *)analyticsService {
- static BTAnalyticsService *analyticsService;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:self];
- analyticsService.flushThreshold = 5;
- });
- // The analytics service may be overridden by unit tests. In that case, return the ivar and not the singleton
- if (_analyticsService) return _analyticsService;
- return analyticsService;
- }
- - (void)sendAnalyticsEvent:(NSString *)eventKind {
- [self.analyticsService sendAnalyticsEvent:eventKind completion:nil];
- }
- - (void)queueAnalyticsEvent:(NSString *)eventKind {
- [self.analyticsService sendAnalyticsEvent:eventKind];
- }
- - (NSDictionary *)metaParameters {
- NSMutableDictionary *metaParameters = [NSMutableDictionary dictionaryWithDictionary:self.metadata.parameters];
- [metaParameters addEntriesFromDictionary:[BTAnalyticsMetadata metadata]];
- return [metaParameters copy];
- }
- - (NSDictionary *)graphQLMetadata {
- return self.metadata.parameters;
- }
- - (NSDictionary *)metaParametersWithParameters:(NSDictionary *)parameters forHTTPType:(BTAPIClientHTTPType)httpType {
- if (httpType == BTAPIClientHTTPTypeBraintreeAPI) {
- return parameters;
- }
- NSMutableDictionary *mutableParameters = [NSMutableDictionary dictionaryWithDictionary:parameters];
- if (httpType == BTAPIClientHTTPTypeGraphQLAPI) {
- mutableParameters[@"clientSdkMetadata"] = [self graphQLMetadata];
- } else if (httpType == BTAPIClientHTTPTypeGateway) {
- mutableParameters[@"_meta"] = [self metaParameters];
- }
- return [mutableParameters copy];
- }
- #pragma mark - HTTP Operations
- - (void)GET:(NSString *)endpoint parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
- [self GET:endpoint parameters:parameters httpType:BTAPIClientHTTPTypeGateway completion:completionBlock];
- }
- - (void)POST:(NSString *)endpoint parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
- [self POST:endpoint parameters:parameters httpType:BTAPIClientHTTPTypeGateway completion:completionBlock];
- }
- - (void)GET:(NSString *)endpoint parameters:(NSDictionary *)parameters httpType:(BTAPIClientHTTPType)httpType completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
- [self fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration * _Nullable configuration, __unused NSError * _Nullable error) {
- if (error != nil) {
- completionBlock(nil, nil, error);
- return;
- }
- [[self httpForType:httpType] GET:endpoint parameters:parameters completion:completionBlock];
- }];
- }
- - (void)POST:(NSString *)endpoint parameters:(NSDictionary *)parameters httpType:(BTAPIClientHTTPType)httpType completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
- [self fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration * _Nullable configuration, __unused NSError * _Nullable error) {
- if (error != nil) {
- completionBlock(nil, nil, error);
- return;
- }
- NSDictionary *postParameters = [self metaParametersWithParameters:parameters forHTTPType:httpType];
- [[self httpForType:httpType] POST:endpoint parameters:postParameters completion:completionBlock];
- }];
- }
- - (BTHTTP *)httpForType:(BTAPIClientHTTPType)httpType {
- if (httpType == BTAPIClientHTTPTypeBraintreeAPI) {
- return self.braintreeAPI;
- } else if (httpType == BTAPIClientHTTPTypeGraphQLAPI) {
- return self.graphQL;
- }
- return self.http;
- }
- - (instancetype)init NS_UNAVAILABLE
- {
- return nil;
- }
- - (void)dealloc
- {
- if (self.http && self.http.session) {
- [self.http.session finishTasksAndInvalidate];
- }
- if (self.braintreeAPI && self.braintreeAPI.session) {
- [self.braintreeAPI.session finishTasksAndInvalidate];
- }
- if (self.graphQL && self.graphQL.session) {
- [self.graphQL.session finishTasksAndInvalidate];
- }
-
- [self.configurationHTTP.session.configuration.URLCache removeAllCachedResponses];
- }
- @end
|