Skip to content

Commit

Permalink
Fix brave/brave-ios#8203: Apple Search Ads Install Attribution (brave…
Browse files Browse the repository at this point in the history
  • Loading branch information
soner-yuksel authored Nov 3, 2023
1 parent a7bcb12 commit 4f2ac91
Show file tree
Hide file tree
Showing 37 changed files with 327 additions and 480 deletions.
8 changes: 8 additions & 0 deletions App/iOS/Delegates/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
UrpLog.log("Failed to initialize user referral program")
}

if Preferences.URP.installAttributionLookupOutstanding.value == nil {
// Similarly to referral lookup, this prefrence should be set if it is a new user
// Trigger install attribution fetch only first launch
Preferences.URP.installAttributionLookupOutstanding.value = isFirstLaunch

SceneDelegate.shouldHandleInstallAttributionFetch = true
}

#if canImport(BraveTalk)
BraveTalkJitsiCoordinator.sendAppLifetimeEvent(
.didFinishLaunching(options: launchOptions ?? [:])
Expand Down
77 changes: 56 additions & 21 deletions App/iOS/Delegates/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
internal var window: UIWindow?
private var windowProtection: WindowProtection?
static var shouldHandleUrpLookup = false
static var shouldHandleInstallAttributionFetch = false

private var cancellables: Set<AnyCancellable> = []
private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "scene-delegate")
Expand Down Expand Up @@ -80,14 +81,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
.store(in: &cancellables)

// Handle URP Lookup at first launch
if SceneDelegate.shouldHandleUrpLookup {
// TODO: Find a better way to do this when multiple windows are involved.
SceneDelegate.shouldHandleUrpLookup = false

if let urp = UserReferralProgram.shared {
browserViewController.handleReferralLookup(urp)
}
}

// Handle Install Attribution Fetch at first launch
if SceneDelegate.shouldHandleInstallAttributionFetch {
SceneDelegate.shouldHandleInstallAttributionFetch = false

if let urp = UserReferralProgram.shared {
browserViewController.handleSearchAdsInstallAttribution(urp)
}
}

// Setup Playlist Car-Play
// TODO: Decide what to do if we have multiple windows
Expand Down Expand Up @@ -196,8 +206,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

// We try to send DAU ping each time the app goes to foreground to work around network edge cases
// (offline, bad connection etc.).
// Also send the ping only after the URP lookup has processed.
if Preferences.URP.referralLookupOutstanding.value == false {
// Also send the ping only after the URP lookup and install attribution has processed.
if Preferences.URP.referralLookupOutstanding.value == false, Preferences.URP.installAttributionLookupOutstanding.value == false {
AppState.shared.dau.sendPingToServer()
}

Expand Down Expand Up @@ -531,29 +541,54 @@ extension SceneDelegate: UIViewControllerRestoration {

extension BrowserViewController {
func handleReferralLookup(_ urp: UserReferralProgram) {

if Preferences.URP.referralLookupOutstanding.value == true {
urp.referralLookup() { referralCode, offerUrl in
// Attempting to send ping after first urp lookup.
// This way we can grab the referral code if it exists, see issue #2586.
AppState.shared.dau.sendPingToServer()
if let code = referralCode {
let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes
let retryDeadline = Date() + retryTime

Preferences.NewTabPage.superReferrerThemeRetryDeadline.value = retryDeadline

// TODO: Set the code in core somehow if we want to support Super Referrals again
// then call updateSponsoredImageComponentIfNeeded
}

guard let url = offerUrl?.asURL else { return }
self.openReferralLink(url: url)
}
performProgramReferralLookup(urp, refCode: UserReferralProgram.getReferralCode())
} else {
urp.pingIfEnoughTimePassed()
}
}

func handleSearchAdsInstallAttribution(_ urp: UserReferralProgram) {
urp.adCampaignLookup() { [weak self] response, error in
guard let self = self else { return }

let refCode = self.generateReferralCode(attributionData: response, fetchError: error)
// Setting up referral code value
// This value should be set before first DAU ping
Preferences.URP.referralCode.value = refCode
Preferences.URP.installAttributionLookupOutstanding.value = false
}
}

private func generateReferralCode(attributionData: AdAttributionData?, fetchError: Error?) -> String {
// Prefix code "001" with BRV for organic iOS installs
var referralCode = "BRV001"

if fetchError == nil, attributionData?.attribution == true, let campaignId = attributionData?.campaignId {
// Adding ASA User refcode prefix to indicate
// Apple Ads Attribution is true
referralCode = "ASA\(String(campaignId))"
}

return referralCode
}

private func performProgramReferralLookup(_ urp: UserReferralProgram, refCode: String?) {
urp.referralLookup(refCode: refCode) { referralCode, offerUrl in
// Attempting to send ping after first urp lookup.
// This way we can grab the referral code if it exists, see issue #2586.
if Preferences.URP.installAttributionLookupOutstanding.value == false {
AppState.shared.dau.sendPingToServer()
}
let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes
let retryDeadline = Date() + retryTime

Preferences.NewTabPage.superReferrerThemeRetryDeadline.value = retryDeadline

guard let url = offerUrl?.asURL else { return }
self.openReferralLink(url: url)
}
}
}

extension UIWindowScene {
Expand Down
26 changes: 0 additions & 26 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,39 +82,13 @@ var package = Package(
.target(
name: "CertificateUtilities",
dependencies: ["Shared"],
resources: [
.copy("Certificates/AmazonRootCA1.cer"),
.copy("Certificates/AmazonRootCA2.cer"),
.copy("Certificates/AmazonRootCA3.cer"),
.copy("Certificates/AmazonRootCA4.cer"),
.copy("Certificates/GlobalSignRootCA_E46.cer"),
.copy("Certificates/GlobalSignRootCA_R1.cer"),
.copy("Certificates/GlobalSignRootCA_R3.cer"),
.copy("Certificates/GlobalSignRootCA_R46.cer"),
.copy("Certificates/GlobalSignRootCA_R5.cer"),
.copy("Certificates/GlobalSignRootCA_R6.cer"),
.copy("Certificates/ISRGRootCA_X1.cer"),
.copy("Certificates/ISRGRootCA_X2.cer"),
.copy("Certificates/SFSRootCAG2.cer"),
],
plugins: ["LoggerPlugin"]
),
.testTarget(
name: "CertificateUtilitiesTests",
dependencies: ["CertificateUtilities", "BraveShared", "BraveCore", "MaterialComponents"],
exclude: [ "Certificates/self-signed.conf" ],
resources: [
.copy("Certificates/root.cer"),
.copy("Certificates/leaf.cer"),
.copy("Certificates/intermediate.cer"),
.copy("Certificates/self-signed.cer"),
.copy("Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-1.cer"),
.copy("Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-2.cer"),
.copy("Certificates/expired.badssl.com/expired.badssl.com-leaf.cer"),
.copy("Certificates/expired.badssl.com/expired.badssl.com-root-ca.cer"),
.copy("Certificates/expired.badssl.com/self-signed.badssl.com.cer"),
.copy("Certificates/expired.badssl.com/untrusted.badssl.com-leaf.cer"),
.copy("Certificates/expired.badssl.com/untrusted.badssl.com-root.cer"),
.copy("Certificates/certviewer/brave.com.cer"),
.copy("Certificates/certviewer/gh.neting.cc.cer"),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import Foundation

extension Sequence {
public extension Sequence {
func asyncForEach(_ operation: (Element) async throws -> Void) async rethrows {
for element in self {
try await operation(element)
Expand Down Expand Up @@ -103,7 +103,7 @@ extension Sequence {
}
}

extension Task where Failure == Error {
public extension Task where Failure == Error {
@discardableResult
static func retry(
priority: TaskPriority? = nil,
Expand All @@ -129,20 +129,20 @@ extension Task where Failure == Error {
}
}

extension Task where Success == Never, Failure == Never {
public extension Task where Success == Never, Failure == Never {
/// Suspends the current task for at least the given duration
/// in seconds.
///
/// If the task is canceled before the time ends,
/// this function throws `CancellationError`.
///
/// This function doesn't block the underlying thread.
public static func sleep(seconds: TimeInterval) async throws {
static func sleep(seconds: TimeInterval) async throws {
try await sleep(nanoseconds: NSEC_PER_MSEC * UInt64(seconds * 1000))
}
}

extension Task where Failure == Error {
public extension Task where Failure == Error {
@discardableResult
static func delayed(
bySeconds seconds: TimeInterval,
Expand Down
69 changes: 67 additions & 2 deletions Sources/BraveShared/Extensions/URLSessionExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@
import Foundation
import Shared
import os.log
import Combine

extension URLSession {
@discardableResult
public func request(
_ url: URL,
method: HTTPMethod = .get,
parameters: [String: Any],
headers: [String: String] = [:],
parameters: [String: Any] = [:],
rawData: Data? = nil,
encoding: ParameterEncoding = .query,
_ completion: @escaping (Result<Any, Error>) -> Void
) -> URLSessionDataTask! {
do {
let request = try buildRequest(
url,
method: method,
headers: headers,
parameters: parameters,
rawData: rawData,
encoding: encoding)

let task = self.dataTask(with: request) { data, response, error in
Expand All @@ -44,6 +49,60 @@ extension URLSession {
return nil
}
}

public func request(
_ url: URL,
method: HTTPMethod = .get,
headers: [String: String] = [:],
parameters: [String: Any] = [:],
rawData: Data? = nil,
encoding: ParameterEncoding = .query
) -> AnyPublisher<Any, Error> {
do {
let request = try buildRequest(
url,
method: method,
headers: headers,
parameters: parameters,
rawData: rawData,
encoding: encoding)

return dataTaskPublisher(for: request)
.tryMap({ data, response in
try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
})
.mapError({ $0 as Error })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
} catch {
Logger.module.error("\(error.localizedDescription)")
return Fail(error: error).eraseToAnyPublisher()
}
}

public func request(
_ url: URL,
method: HTTPMethod = .get,
headers: [String: String] = [:],
parameters: [String: Any] = [:],
rawData: Data? = nil,
encoding: ParameterEncoding = .query
) async throws -> (Any, URLResponse) {
do {
let request = try buildRequest(
url,
method: method,
headers: headers,
parameters: parameters,
rawData: rawData,
encoding: encoding)

return try await data(for: request)
} catch {
Logger.module.error("\(error.localizedDescription)")
throw error
}
}
}

extension URLSession {
Expand All @@ -56,22 +115,28 @@ extension URLSession {
}

public enum ParameterEncoding {
case textPlain
case json
case query
}

private func buildRequest(
_ url: URL,
method: HTTPMethod,
headers: [String: String] = [:],
headers: [String: String],
parameters: [String: Any],
rawData: Data?,
encoding: ParameterEncoding
) throws -> URLRequest {

var request = URLRequest(url: url)
request.httpMethod = method.rawValue
headers.forEach({ request.setValue($0.value, forHTTPHeaderField: $0.key) })
switch encoding {
case .textPlain:
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
request.httpBody = rawData

case .json:
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
Expand Down
1 change: 1 addition & 0 deletions Sources/CertificateUtilities/BraveCertificateUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import Foundation
import Shared

public struct BraveCertificateUtils {
/// Formats a hex string
Expand Down
Loading

0 comments on commit 4f2ac91

Please sign in to comment.