DataDome iOS SDK 2.0

VersionVersion LicenseLicense PlatformPlatform

Install

Swift package manager

DataDomeSDK is available on Swift Package Manager. To get the SDK integrated in your project, follow the steps below:

  1. Go to "Xcode > File > Swift Packages > Add Package Dependency", select the target where to integrate DataDome
  2. Paste the following git URL in the search bar https://github.com/DataDome/datadome-ios-package
  3. Select DataDomeSDK and press Add.

CocoaPods

Alternatively, you can get the DataDomeSDK on CocoaPods. Simply add the following line in your Podfile:

pod "DataDomeSDK"

Run pod install to download and integrate the framework into your project.

Note
Please note that starting with SDK version 2.5.0, we use the XCFramework format. Old CocoaPods versions don't handle this new format well. Please make sure you are using CocoaPods version 1.10 or later to avoid any linking issue.

Carthage

DataDomeSDK is available on Carthage. To get the SDK integrated in your project, follow the steps below:

  1. Create a Cartfile if not already created.
  2. Add the following dependency in your project Cartfile
binary "https://package.datadome.co/ios/DataDomeSDK.json"
  1. Run the following command to fetch and integrate the package
carthage update --use-xcframeworks

Note
Please note that Carthage supports the XCFramework format starting from version 0.38.0. With SDK version 2.5.0, we use the XCFramework format. Old Carthage builds don't handle this new format well. Please make sure you are using Carthage version 0.38.0 or later to avoid any linking issue.

Getting Started

  1. Run your application. It will crash and return the following log:
Fatal error: [DataDome] Missing DataDomeKey (Your client side key) in your Info.plist
  1. In your Info.plist, add a new entry with String type, use DataDomeKey as "key" and your actual client side key as "value".
  2. Now you can run the app without crashing. You should get a log confirming the SDK is running:
[DataDome] Version x.y.z

Congrats, the DataDome framework is now properly integrated.

Logging

If you need to view the logs produced by the framework, you can configure the log level to control the detail level in the logs you get:

import DataDomeSDK
DataDomeSDK.setLogLevel(level: .verbose)

By default, the framework is completely silent.

The following table contains different logging levels that you may consider using:

Level

Description

verbose

Everything is logged

info

Info messages, warnings and errors are shown

warning

Only warning and error messages are printed

error

Only errors are printed

none

Silent mode (default)

Share cookies between native and web

If you are using WKWebView in your application to display web pages that are protected with DataDome, you can share the generated cookie by resolving a Captcha on the native part of the app, therefore a second Captcha won't be required on the web-based part of the app.
To share the DataDome cookie from the SDK to your WKWebView instance, use the following API:

import DataDomeSDK

if let cookie = DataDomeSDK.getCookie() {
  webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}

The getCookie API returns the stored Captcha generated by the last resolved Captcha, else it returns nothing if there was no Captcha stored.

If your web page is displayed before the app sends any request natively, you can consider providing the cookie stored by your web view after resolving the Captcha on the web view side. Alternatively, if you need to store a cookie, use the following API:

import DataDomeSDK

DataDomeSDK.setCookie(cookie: cookie)

Disable method swizzling

The DataDome Framework is using method swizzling to setup a proxy that listens to all URLRequests performed by the app. The framework automatically detects blocked requests, performs a dynamic lookup on the top view controller in your app hierarchy and attaches a Captcha view controller to it. Once resolved, the framework will dismiss the Captcha view controller and will retry the blocked request with a newly generated cookie. All this is done "under the hood".

If you want to disable the method swizzling and manually instruct the framework with which requests to monitor instead of monitoring all traffic, and/or you want to manually present and dismiss the Captcha controller, please consider the following steps:

  1. In your Info.plist, add a new entry with Boolean type, using DataDomeProxyEnabled as key and NO as value. This will disable method swizzling in the framework.
  2. For each request to be protected with DataDome, use the following methods to create the data task:
func protectedDataTask(with url: URL,
                       captchaDelegate: CaptchaDelegate?,
                       completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) 
-> URLSessionDataTask
func protectedDataTask(with request: URLSessionRequest,
                       captchaDelegate: CaptchaDelegate?,
                       completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) 
-> URLSessionDataTask

Only the URLSessionDataTasks created by the two methods above are monitored and protected by DataDome.

Below is a sample code to create and execute a data task:

import DataDomeSDK


guard let url = URL(string: "https://datadome.co") else {
    return
}

let task = URLSession.shared.protectedDataTask(with: url, captchaDelegate: self, completionHandler: { _, _, _ in
    print("Request completed")
})
        
task.resume()

To manually manage the presentation of the Captcha view controller, you need to implement the CaptchaDelegate protocol:

import DataDomeSDK

extension MyViewController: CaptchaDelegate {
    func present(captchaController controller: UIViewController) {
        self.navigationController?.present(controller, animated: true, completion: nil)
    }
    
    func dismiss(captchaController controller: UIViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
}

Important
If you are creating your own URLSession, make sure to NOT user URLSessionConfiguration.ephemeral as it may result in a captcha loop. Ephemeral sessions does not use disk based Cookie Storage. Here an exemple of a custom url session

var sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.httpCookieStorage = HTTPCookieStorage.shared
sessionConfiguration.httpShouldSetCookies = true

let session = URLSession(configuration: sessionConfiguration)

Using Combine & URLSession.DataTaskPublisher

If you are using Combine and DataTaskPublisher to perform networking requests in your app, we have you covered.
Use the following apis to create a DataTaskPublisher with the DataDome operators

/// Provides a publisher with downstream DataDome operators
/// - Parameters:
///   - url: The url to be requested
///   - captchaDelegate: The captcha delegate
/// - Returns: A publisher with the DataDome operators
func protectedDataTaskPublisher(forURL url: URL,
                                captchaDelegate: CaptchaDelegate?)
    -> AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>

Or by using an URLSessionRequest instance as parameter

/// Provides a publisher with downstream DataDome operators
/// - Parameters:
///   - request: The request to be processed
///   - captchaDelegate: The captcha delegate
/// - Returns: A publisher with the DataDome operators
func protectedDataTaskPublisher(forRequest request: URLRequest,
                                captchaDelegate: CaptchaDelegate?)
-> AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>

Here an example on how to use the DataDome protected publisher

URLSession
.shared
.protectedDataTaskPublisher(forURL: url, captchaDelegate: nil)
.sink(receiveCompletion: { completion in
      switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error.localizedDescription)
      }
}, receiveValue: { response in
     guard let httpResponse = response.response as? HTTPURLResponse else {
       print("Invalid response")
       return
     }

     print("Did receive response with code \(httpResponse.statusCode)")
  }
})

You can use any other operators to validate or transform the publisher or part of its elements. For more details, please visit Apple Documentation

Using Alamofire / Moya

If you are using Alamofire or Moya to build your app's networking layer, we have you covered. Install the DataDome Alamofire plugin to protect your requests using the DataDome SDK.

Cocoapods

To install the plugin, use the following Cocoapods instruction:

pod "DataDomeAlamofire"

Carthage

DataDomeAlamofire is available on Carthage. To get the SDK integrated in your project, follow the steps below:

  1. Create a Cartfile if not already created.
  2. Add the following dependency in your project Cartfile
github "DataDome/datadome-alamofire-package"
  1. Run the following command to fetch and integrate the package
carthage update

Make sure method swizzling for URLSession is disabled to avoid any interference with Alamofire's "under the hood" URLSession usage.
In your Info.plist, add a new entry with Boolean type, using DataDomeProxyEnabled as key and NO as value. This will disable method swizzling in the framework.

Create your Alamofire Session Manager as shows in the example below:

import DataDomeAlamofire

let configuration = URLSessionConfiguration.default
configuration.headers = .default
configuration.httpCookieStorage = HTTPCookieStorage.shared
configuration.httpShouldSetCookies = true

let dataDome = AlamofireInterceptor()
let interceptor = Interceptor(adapter: dataDome.sessionAdapter, 
                              retrier: dataDome.sessionRetrier)
 
let alamofireSessionManager = Session(configuration: configuration, 
                                      interceptor: interceptor)

The remainder of your app won't change. Only make sure to use the created alamofireSessionManager to perform your requests.

Important
When using Alamofire, make sure you call .validate() for each request to make sure Alamofire does call the retry function and hands the execution to the DataDome SDK in case of a 403 response with a Captcha Challenge.

self.alamofireSessionManager
        .request(endpoint)
        //validate here is mandatory
        .validate()
        .responseData { response in
}

When using Moya, make sure you explicitly implement validationType in your service implementation

extension MyService: TargetType {
    var validationType: ValidationType {
        let type = ValidationType.successAndRedirectCodes
        return type
    }
}

The validationType attribute will make Moya call the .validate() function when handing the request to Alamofire.

Important
If you are creating an instance of URLRequest and passing it to Alamofire Session Manager, make sure to hint iOS to handle cookies otherwise, you may experience captcha loops in the app.

var request = URLRequest(url: url)
request.httpShouldHandleCookies = true
session.request(request).validate()

Using AFNetworking client

We forked AFNetworking to adapt it to DataDomeSDK.
To install DataDomeAFNetworking, use the following statement.

pod 'DataDomeAFNetworking', '~> 4.0.1'

You should use the above dependency instead of AFNetworking. The library contains the same source code as AFNetworking with an adaptation to make it work with DataDome.
Replace your import statements as the following.

// AFNetworking import statement to be replaced
#import <AFNetworking/AFNetworking.h>
  
// The new import statement to be used
@import DataDomeAFNetworking;

No other changes are required to keep your code base working with AFNetworking and DataDome.

Using Apollo GraphQL Client

If your api is a GraphQL api, the chances are that you are using Apollo SDK. If so, we have a dedicated framework called DataDomeApollo designed to ease the DataDome SDK integration.

To install the plugin, use the following Cocoapods instruction:

pod "DataDomeApollo"

Make sure method swizzling for URLSession is disabled to avoid any interference with Apollo's "under the hood" URLSession usage.
In your Info.plist, add a new entry with Boolean type, using DataDomeProxyEnabled as key and NO as value. This will disable method swizzling in the framework.

We use the Interceptor pattern built in the Apollo SDK to make sure we intercept all requests and protect them. The following is the init of the DataDome Interceptor

let interceptor = DataDomeResponseInterceptor()

The interceptor could be used then in your pipeline. It will intercept all requests, catch requests blocked by DataDome, display the captcha and eventually retry the request automatically once the captcha is resolved.

Important
The order in defining the interceptors is important in Apollo. Make sure the DataDome response Interceptor is the first interceptor after the NetworkFetchInterceptor.

Alternatively, you can use the DataDomeInterceptorProvider which is a pre-built sequence of interceptors ready to be used by your app. This provider is providing the same interceptors as the Legacy Interceptor provider. You can customise the interceptors to be added in the provider or use the default set.

private(set) lazy var apollo: ApolloClient = {
        // Create your own store needed to init the DataDomeInterceptor provider
        let cache = InMemoryNormalizedCache()
        let store = ApolloStore(cache: cache)
        
        // Use DataDomeURLSessionClient to enable updating the user-agent
        let client = DataDomeURLSessionClient()
        
        // Create the DataDome Interceptor Provider
        let provider = DataDomeInterceptorProvider(store: store, client: client)
        
        // Create your GraphQL URL
        guard let url = URL(string: "YOUR_ENDPOINT") else {
            fatalError("Unable to create url")
        }
        
        let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider,
                                                                 endpointURL: url)
        
        // Create the client with the request chain transport
        return ApolloClient(networkTransport: requestChainTransport,
                            store: store)
    }()

In the above exemple we used the default configuration of DataDomeInterceptorProvider. You can specify custom interceptors using the following init method

/// Creates an interceptor provider with a setup instance of DataDome
    /// - Parameters:
    ///   - store: The apollo store
    ///   - client: The URLSession client
    ///   - preFetchInterceptors: The list of interceptors to go before the fetch operation
    ///   - fetchInterceptor: The fetch operation
    ///   - postFetchInterceptors: The list of interceptors to go after the fetch operation
    public init(store: ApolloStore,
                client: URLSessionClient,
                preFetchInterceptors: [ApolloInterceptor] = [],
                fetchInterceptor: ApolloInterceptor? = nil,
                postFetchInterceptors: [ApolloInterceptor] = [])

preFetchInterceptors is a set of interceptors to be added before firing the request. By default we add:

  • CacheReadInterceptor

fetchInterceptor is the interceptor that will execute the request. By default, we use:

  • NetworkFetchInterceptor

The DataDomeResponseInterceptor is then added to the chain just after the fetch interceptor.

postFetchInterceptors is a set of interceptors to be added after the fetch interceptor. By default we add:

  • ResponseCodeInterceptor
  • JSONResponseParsingInterceptor
  • AutomaticPersistedQueryInterceptor
  • CacheWriteInterceptor