Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Fix #8203: Apple Search Ads Install Attribution #8194

Merged
merged 21 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dbe073f
Initial code with ad attribution fetch method rough work
soner-yuksel Sep 29, 2023
18255d6
URP servie adds AdServices AttributionApiRequest
soner-yuksel Sep 29, 2023
34e8837
Adding ad campaign lookup token in delegate
soner-yuksel Sep 29, 2023
352e16c
Adding combine and async await methods in session
soner-yuksel Oct 4, 2023
63b774d
Adding ad campaign Token fetch and passing it to service
soner-yuksel Oct 4, 2023
6e2ba19
URP Service Separate
soner-yuksel Oct 5, 2023
d603d20
Removing Certificate pinning certificates and certificates for tests
soner-yuksel Oct 31, 2023
e9d003a
Using referral methods to fetch the information
soner-yuksel Oct 5, 2023
4125862
Adding Code reference user prefix
soner-yuksel Oct 5, 2023
3a0afe1
Adding Ad Attribution Json Object Datas
soner-yuksel Oct 6, 2023
73e72c9
Adding comments code generator
soner-yuksel Oct 6, 2023
5ed5dfa
Adding referral code token atribution for release
soner-yuksel Oct 6, 2023
6c32bb0
Changes for separating install attribution
soner-yuksel Oct 17, 2023
58412ca
Pull Request comments are addressed
soner-yuksel Oct 17, 2023
ca75c29
Removing 3 digit campaign id criteria request
soner-yuksel Oct 17, 2023
2774ae0
Pull request comments are addressed
soner-yuksel Oct 25, 2023
3766a52
Adding a different channel for staging DAU process ping
soner-yuksel Oct 30, 2023
79a6ee9
The Organic install code is change with debug server domain in request
soner-yuksel Nov 1, 2023
a78ff18
Adding lookup outstanding to ping server for ad attribution
soner-yuksel Nov 1, 2023
ce12736
Changing DAU server channel variable
soner-yuksel Nov 2, 2023
586cb3e
Fixing ping param channel for beta and dev and test
soner-yuksel Nov 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/github.com.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
iccub marked this conversation as resolved.
Show resolved Hide resolved

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