diff --git a/ContentPassExample/ContentPassExample/ContentView.swift b/ContentPassExample/ContentPassExample/ContentView.swift index c245481..d4f8391 100644 --- a/ContentPassExample/ContentPassExample/ContentView.swift +++ b/ContentPassExample/ContentPassExample/ContentView.swift @@ -60,6 +60,6 @@ struct ContentView: View { viewModel.countImpression() } .buttonStyle(.borderedProminent) - .opacity(viewModel.isError || !viewModel.isAuthenticated ? 0 : 1) + .opacity(viewModel.isError ? 0 : 1) } } diff --git a/README.md b/README.md index dd3ab40..2a84079 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,12 @@ Since we don't monitor the device's connection state you need to tell the SDK th contentPass.recoverFromError() ``` -### Couting an impression - -Counting an impression is as easy as calling the function `countImpression(completionHandler:)`. A user has to be authenticated and have an active subscription applicable to your scope for this to work. +### Counting an impression +`countImpression` method counts impressions for billing purposes. This method must be invoked whenever a user views a piece +of content, independently of authentication state. If the current user is authenticated the impression will automatically +be logged as paid ad-free impression to calculate the publisher compensation. As the total amount of impressions is required +for billing as well, this method also counts sampled impressions of non-subscribers. Counting an impression is as easy as +calling the function `countImpression(completionHandler:)` ```swift contentPass.countImpression { result in diff --git a/Sources/ContentPass/ContentPass.swift b/Sources/ContentPass/ContentPass.swift index 1c4250c..31975ca 100644 --- a/Sources/ContentPass/ContentPass.swift +++ b/Sources/ContentPass/ContentPass.swift @@ -2,6 +2,8 @@ import AppAuth import AuthenticationServices import UIKit +let samplingRate: Double = 0.05 + /// Functions that enable you to react to changes in the contentpass sdk's state. public protocol ContentPassDelegate: AnyObject { /// A function that enables you to react to a change in contentpass state. @@ -43,7 +45,7 @@ public class ContentPass: NSObject { /// This is always up to date but to be notified of changes in state, be sure to register a `ContentPassDelegate` as the parent object's `delegate`. /// /// For the possible values and their meaning see `ContentPass.State`. - internal (set) public var state = State.initializing { didSet { didSetState(state) } } + internal(set) public var state = State.initializing { didSet { didSetState(state) } } /// The object that acts as the delegate of the contentpass sdk. /// @@ -134,26 +136,39 @@ public class ContentPass: NSObject { validateAuthState() } - /// Count an impression for the logged in user. + /// Count an impression for user. /// - /// A user needs to be authenticated and have a subscription applicable to your service. - /// - Parameter completionHandler: On a successful counting of the impression, the Result is a `success`. If something went wrong, you'll be supplied with an appropriate error case. The error `ContentPassError.badHTTPStatusCode(404)` most probably means that your user has no applicable subscription. + /// If user has a valid subscription, a paid impression will be counted. Additionally a sampled impression will be + /// counted for all users, no matter if they have a valid subscription or not. + /// - Parameter completionHandler: On a successful counting of the impression, the Result is a `success`. If something went wrong, + /// you'll be supplied with an appropriate error case. public func countImpression(completionHandler: @escaping (Result) -> Void) { - let impressionID = UUID() - let propertyId = propertyId.split(separator: "-").first! - let request = URLRequest(url: URL(string: "\(configuration.apiUrl)/pass/hit?pid=\(propertyId)&iid=\(impressionID)&t=pageview")!) + let dispatchGroup = DispatchGroup() + var errors: [Error] = [] + + if state == .authenticated(hasValidSubscription: true) { + dispatchGroup.enter() + countPaidImpression { result in + if case .failure(let error) = result { + errors.append(error) + } + dispatchGroup.leave() + } + } - oidAuthState?.fireRequest(urlRequest: request) { _, response, error in - if let error = error { + dispatchGroup.enter() + countSampledImpression { result in + if case .failure(let error) = result { + errors.append(error) + } + dispatchGroup.leave() + } + + dispatchGroup.notify(queue: .main) { + if let error = errors.first { completionHandler(.failure(error)) - } else if let httpResponse = response as? HTTPURLResponse { - if httpResponse.statusCode == 200 { - completionHandler(.success(())) - } else { - completionHandler(.failure(ContentPassError.badHTTPStatusCode(httpResponse.statusCode))) - } } else { - completionHandler(.failure(ContentPassError.corruptedResponseFromWeb)) + completionHandler(.success(())) } } } @@ -190,6 +205,67 @@ public class ContentPass: NSObject { super.init() } + private func countPaidImpression(completionHandler: @escaping (Result) -> Void) { + let impressionID = UUID() + let propertyId = propertyId.split(separator: "-").first! + let request = URLRequest(url: URL(string: "\(configuration.apiUrl)/pass/hit?pid=\(propertyId)&iid=\(impressionID)&t=pageview")!) + + oidAuthState?.fireRequest(urlRequest: request) { _, response, error in + if let error = error { + completionHandler(.failure(error)) + } else if let httpResponse = response as? HTTPURLResponse { + if httpResponse.statusCode == 200 { + completionHandler(.success(())) + } else { + completionHandler(.failure(ContentPassError.badHTTPStatusCode(httpResponse.statusCode))) + } + } else { + completionHandler(.failure(ContentPassError.corruptedResponseFromWeb)) + } + } + } + + private func countSampledImpression(completionHandler: @escaping (Result) -> Void) { + let generatedSample = Double.random(in: 0...1) + if generatedSample >= samplingRate { + completionHandler(.success(())) + return + } + + let instanceId = UUID().uuidString + let publicId = propertyId.prefix(8) + var request = URLRequest(url: URL(string: "\(configuration.apiUrl)/stats")!) + request.httpMethod="POST" + request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") + let body: [String: Any] = [ + "ea": "load", + "ec": "tcf-sampled", + "cpabid": instanceId, + "cppid": publicId, + "cpsr": samplingRate + ] + do { + request.httpBody = try JSONSerialization.data(withJSONObject: body) + } catch { + completionHandler(.failure(error)) + return + } + + URLSession.shared.dataTask(with: request) {_, response, error in + if let error = error { + completionHandler(.failure(error)) + } else if let httpResponse = response as? HTTPURLResponse { + if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { + completionHandler(.success(())) + } else { + completionHandler(.failure(ContentPassError.badHTTPStatusCode(httpResponse.statusCode))) + } + } else { + completionHandler(.failure(ContentPassError.corruptedResponseFromWeb)) + } + }.resume() + } + private func validateAuthState() { guard let authState = oidAuthState,