diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index 488e447f17f..d4959f9e69a 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -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 ?? [:]) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 0f8a04f3da2..e8d38da7b0c 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -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 = [] private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "scene-delegate") @@ -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 @@ -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() } @@ -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 { diff --git a/Package.swift b/Package.swift index eb7cdc408a7..201752d59c0 100644 --- a/Package.swift +++ b/Package.swift @@ -82,21 +82,6 @@ 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( @@ -104,17 +89,6 @@ var package = Package( 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"), ] diff --git a/Sources/Brave/Extensions/AsyncAwait.swift b/Sources/BraveShared/Extensions/AsyncAwait.swift similarity index 94% rename from Sources/Brave/Extensions/AsyncAwait.swift rename to Sources/BraveShared/Extensions/AsyncAwait.swift index 09f3214dd36..3e17b9cdb4f 100644 --- a/Sources/Brave/Extensions/AsyncAwait.swift +++ b/Sources/BraveShared/Extensions/AsyncAwait.swift @@ -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) @@ -103,7 +103,7 @@ extension Sequence { } } -extension Task where Failure == Error { +public extension Task where Failure == Error { @discardableResult static func retry( priority: TaskPriority? = nil, @@ -129,7 +129,7 @@ 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. /// @@ -137,12 +137,12 @@ extension Task where Success == Never, Failure == Never { /// 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, diff --git a/Sources/BraveShared/Extensions/URLSessionExtensions.swift b/Sources/BraveShared/Extensions/URLSessionExtensions.swift index d9e3c0b84be..3a503a1b319 100644 --- a/Sources/BraveShared/Extensions/URLSessionExtensions.swift +++ b/Sources/BraveShared/Extensions/URLSessionExtensions.swift @@ -5,13 +5,16 @@ 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) -> Void ) -> URLSessionDataTask! { @@ -19,7 +22,9 @@ extension URLSession { 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 @@ -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 { + 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 { @@ -56,6 +115,7 @@ extension URLSession { } public enum ParameterEncoding { + case textPlain case json case query } @@ -63,8 +123,9 @@ extension URLSession { private func buildRequest( _ url: URL, method: HTTPMethod, - headers: [String: String] = [:], + headers: [String: String], parameters: [String: Any], + rawData: Data?, encoding: ParameterEncoding ) throws -> URLRequest { @@ -72,6 +133,10 @@ extension URLSession { 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) diff --git a/Sources/CertificateUtilities/BraveCertificateUtils.swift b/Sources/CertificateUtilities/BraveCertificateUtils.swift index f6765d1269e..0ad64e00c65 100644 --- a/Sources/CertificateUtilities/BraveCertificateUtils.swift +++ b/Sources/CertificateUtilities/BraveCertificateUtils.swift @@ -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 diff --git a/Sources/CertificateUtilities/CertificatePinning.swift b/Sources/CertificateUtilities/CertificatePinning.swift deleted file mode 100644 index 115432ac6ca..00000000000 --- a/Sources/CertificateUtilities/CertificatePinning.swift +++ /dev/null @@ -1,194 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import Foundation -import Shared -import os.log - -// Taken from: https://github.com/Brandon-T/Jarvis and modified to simplify - -public class PinningCertificateEvaluator: NSObject, URLSessionDelegate { - struct ExcludedPinningHostUrls { - static let urls = [ - "laptop-updates.brave.com", - "updates.bravesoftware.com", - "updates-cdn.bravesoftware.com", - ] - } - - private let hosts: [String] - private let certificates: [SecCertificate] - private let options: PinningOptions - - public init(hosts: [String], options: PinningOptions = [.default, .validateHost, .anchorSpecificAndSystemTrusts]) { - self.hosts = hosts - self.options = options - - // Load certificates in the main bundle.. - self.certificates = { - let paths = Set( - [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { - Bundle.module.paths(forResourcesOfType: $0, inDirectory: nil) - }.joined()) - - return paths.compactMap({ path -> SecCertificate? in - guard let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData else { - return nil - } - return SecCertificateCreateWithData(nil, certificateData) - }) - }() - } - - public init(hosts: [String: SecCertificate], options: PinningOptions = [.default, .validateHost, .anchorSpecificAndSystemTrusts]) { - self.hosts = hosts.map({ $0.key }) - self.certificates = hosts.map({ $0.value }) - self.options = options - } - - public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - - DispatchQueue.global(qos: .userInitiated).async { - // Certificate pinning - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - if let serverTrust = challenge.protectionSpace.serverTrust { - do { - let host = challenge.protectionSpace.host - if ExcludedPinningHostUrls.urls.contains(host) { - return completionHandler(.performDefaultHandling, nil) - } - - if !self.canPinHost(host) { - self.fatalErrorInDebugModeIfPinningFailed() - throw self.error(reason: "Host not specified for pinning: \(host)") - } - - try self.evaluate(serverTrust, forHost: host) - return completionHandler(.useCredential, URLCredential(trust: serverTrust)) - } catch { - Logger.module.error("\(error.localizedDescription)") - self.fatalErrorInDebugModeIfPinningFailed() - return completionHandler(.cancelAuthenticationChallenge, nil) - } - } - self.fatalErrorInDebugModeIfPinningFailed() - return completionHandler(.cancelAuthenticationChallenge, nil) - } - return completionHandler(.performDefaultHandling, nil) - } - } - - private func canPinHost(_ host: String) -> Bool { - return hosts.contains(host) - } - - private func error(reason: String) -> NSError { - return NSError(domain: "com.brave.pinning-certificate-evaluator", code: -1, userInfo: [NSLocalizedDescriptionKey: reason]) - } - - func evaluate(_ trust: SecTrust, forHost host: String) throws { - // Certificate validation - guard !certificates.isEmpty else { - throw error(reason: "Empty Certificates") - } - - // Certificate anchoring - if options.contains(.anchorSpecificTrustsOnly) || options.contains(.anchorSpecificAndSystemTrusts) { - // Add the certificates to the trust - guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess else { - throw error(reason: "Certificate Anchor Failed") - } - - if options.contains(.anchorSpecificTrustsOnly) { - // Trust only the passed in certificates (true) - // - // This is the default behaviour, however we do it explicitly to throw an exception - // immediately upon failure - // The default behaviour will silently ignore the exception until validation - guard SecTrustSetAnchorCertificatesOnly(trust, true) == errSecSuccess else { - throw error(reason: "Self-Signed Certificate Anchor Only Failed") - } - } else { - // Trust also the built in system certificates (false) - guard SecTrustSetAnchorCertificatesOnly(trust, false) == errSecSuccess else { - throw error(reason: "Certificate Anchor Only Failed") - } - } - } - - // Default validation - if options.contains(.default) { - guard SecTrustSetPolicies(trust, SecPolicyCreateSSL(true, nil)) == errSecSuccess else { - throw error(reason: "Trust Set Policies Failed") - } - - var err: CFError? - if !SecTrustEvaluateWithError(trust, &err) { - if let err = err as Error? { - throw error(reason: "Trust Evaluation Failed: \(err)") - } - - throw error(reason: "Unable to Evaluate Trust") - } - } - - // Host validation - if options.contains(.validateHost) { - guard SecTrustSetPolicies(trust, SecPolicyCreateSSL(true, host as CFString)) == errSecSuccess else { - throw error(reason: "Trust Set Policies for Host Failed") - } - - var err: CFError? - if !SecTrustEvaluateWithError(trust, &err) { - if let err = err as Error? { - throw error(reason: "Trust Evaluation Failed: \(err)") - } - - throw error(reason: "Unable to Evaluate Trust") - } - } - - // Certificate binary matching - let serverCertificates = Set( - (SecTrustCopyCertificateChain(trust) as? [SecCertificate] ?? []) - .map({ SecCertificateCopyData($0) as Data }) - ) - - // Set Certificate validation - let clientCertificates = Set(certificates.compactMap({ SecCertificateCopyData($0) as Data })) - if serverCertificates.isDisjoint(with: clientCertificates) { - throw error(reason: "Pinning Failed") - } - } - - private func fatalErrorInDebugModeIfPinningFailed() { - if !AppConstants.buildChannel.isPublic { - assertionFailure("An SSL Pinning error has occurred") - } - } - - public struct PinningOptions: OptionSet { - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// System's default pinning policies - public static let `default` = PinningOptions(rawValue: 1 << 0) - - /// Host Validation - public static let validateHost = PinningOptions(rawValue: 1 << 1) - - /// Anchor ONLY the specified trusts passed to `PinningCertificateEvaluator.evaluate(trust:host:)` - public static let anchorSpecificTrustsOnly = PinningOptions(rawValue: 1 << 2) - - /// Anchor the specified trusts passed to `PinningCertificateEvaluator.evaluate(trust:host:)` - /// Also anchors the default trusts of the System - public static let anchorSpecificAndSystemTrusts = PinningOptions(rawValue: 1 << 3) - - /// Default pinning policies + Host Validation + Anchor the specified trusts + Anchor system trusts - public static let all: PinningOptions = [.default, .validateHost, .anchorSpecificAndSystemTrusts] - } -} diff --git a/Sources/CertificateUtilities/Certificates/AmazonRootCA1.cer b/Sources/CertificateUtilities/Certificates/AmazonRootCA1.cer deleted file mode 100644 index 86b7dcd0b41..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/AmazonRootCA1.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/AmazonRootCA2.cer b/Sources/CertificateUtilities/Certificates/AmazonRootCA2.cer deleted file mode 100644 index 86d70df56ac..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/AmazonRootCA2.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/AmazonRootCA3.cer b/Sources/CertificateUtilities/Certificates/AmazonRootCA3.cer deleted file mode 100644 index 7a991b766c3..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/AmazonRootCA3.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/AmazonRootCA4.cer b/Sources/CertificateUtilities/Certificates/AmazonRootCA4.cer deleted file mode 100644 index 10ef4413d71..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/AmazonRootCA4.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_E46.cer b/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_E46.cer deleted file mode 100644 index 56039c090ee..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_E46.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R1.cer b/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R1.cer deleted file mode 100644 index 1e6967febb8..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R1.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R3.cer b/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R3.cer deleted file mode 100644 index 232c4b6121f..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R3.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R46.cer b/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R46.cer deleted file mode 100644 index e7ad80617e0..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R46.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R5.cer b/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R5.cer deleted file mode 100644 index fed8dab45f4..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R5.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R6.cer b/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R6.cer deleted file mode 100644 index 3492b9555d8..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/GlobalSignRootCA_R6.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/ISRGRootCA_X1.cer b/Sources/CertificateUtilities/Certificates/ISRGRootCA_X1.cer deleted file mode 100644 index 9d2132e7f1e..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/ISRGRootCA_X1.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/ISRGRootCA_X2.cer b/Sources/CertificateUtilities/Certificates/ISRGRootCA_X2.cer deleted file mode 100644 index 0f5f95fee95..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/ISRGRootCA_X2.cer and /dev/null differ diff --git a/Sources/CertificateUtilities/Certificates/SFSRootCAG2.cer b/Sources/CertificateUtilities/Certificates/SFSRootCAG2.cer deleted file mode 100644 index 42f3c1b9ea8..00000000000 Binary files a/Sources/CertificateUtilities/Certificates/SFSRootCAG2.cer and /dev/null differ diff --git a/Sources/Growth/DAU.swift b/Sources/Growth/DAU.swift index 3322cde7ce9..c697c437873 100644 --- a/Sources/Growth/DAU.swift +++ b/Sources/Growth/DAU.swift @@ -12,8 +12,16 @@ public class DAU { public static let defaultWoiDate = "2016-01-04" private static let apiVersion = 1 - private static let baseUrl = "https://laptop-updates.brave.com/\(apiVersion)/usage/ios?platform=ios" + private static var baseUrl: String { + get { + let domain = AppConstants.buildChannel.isPublic + ? "https://laptop-updates.brave.com/" + : "https://laptop-updates.bravesoftware.com/" + + return "\(domain)\(apiVersion)/usage/ios?platform=ios" + } + } /// Number of seconds that determins when a user is "active" private let pingRefreshDuration = 5.minutes @@ -59,11 +67,6 @@ public class DAU { /// Sends ping to server and returns a boolean whether a timer for the server call was scheduled. /// A user needs to be active for a certain amount of time before we ping the server. @discardableResult public func sendPingToServer() -> Bool { - if AppConstants.buildChannel == .debug || AppConstants.buildChannel == .enterprise { - Logger.module.info("Development build detected, no server ping.") - return false - } - guard Preferences.DAU.sendUsagePing.value else { Logger.module.debug("DAU ping disabled by the user.") return false @@ -239,7 +242,7 @@ public class DAU { } func channelParam(for channel: AppBuildChannel = AppConstants.buildChannel) -> URLQueryItem { - return URLQueryItem(name: "channel", value: channel.serverChannelParam) + return URLQueryItem(name: "channel", value: channel.dauServerChannelParam) } func braveCoreParams(for braveStats: BraveStats) -> [URLQueryItem] { diff --git a/Sources/Growth/GrowthPreferences.swift b/Sources/Growth/GrowthPreferences.swift index 409795105e6..04853bd9ed7 100644 --- a/Sources/Growth/GrowthPreferences.swift +++ b/Sources/Growth/GrowthPreferences.swift @@ -29,6 +29,7 @@ extension Preferences { public static let referralCodeDeleteDate = Option(key: "urp.referral.delete-date", default: nil) /// Whether the ref code lookup has still yet to occur public static let referralLookupOutstanding = Option(key: "urp.referral.lookkup-completed", default: nil) + public static let installAttributionLookupOutstanding = Option(key: "install.attribution.lookup-completed", default: nil) } public final class Review { diff --git a/Sources/Growth/URP/AdAttributionData.swift b/Sources/Growth/URP/AdAttributionData.swift new file mode 100644 index 00000000000..8f47c018998 --- /dev/null +++ b/Sources/Growth/URP/AdAttributionData.swift @@ -0,0 +1,70 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os.log + +public struct AdAttributionData { + // A value of true returns if a user clicks an Apple Search Ads impression up to 30 days before your app download. + // If the API can’t find a matching attribution record, the attribution value is false. + public let attribution: Bool + // The identifier of the organization that owns the campaign. + // organizationId is the same as your account in the Apple Search Ads UI. + public let organizationId: Int? + // The type of conversion is either Download or Redownload. + public let conversionType: String? + // The unique identifier for the campaign. + public let campaignId: Int + // The country or region for the campaign. + public let countryOrRegion: String? + + init(attribution: Bool, organizationId: Int? = nil, conversionType: String? = nil, campaignId: Int, countryOrRegion: String? = nil) { + self.attribution = attribution + self.organizationId = organizationId + self.conversionType = conversionType + self.campaignId = campaignId + self.countryOrRegion = countryOrRegion + } +} + +enum SerializationError: Error { + case missing(String) + case invalid(String, Any) +} + +extension AdAttributionData { + init(json: [String: Any]?) throws { + guard let json = json else { + throw SerializationError.invalid("Invalid json Dictionary", "") + } + + // Attribution and campaignId are the major properties here + // They will indicate if the Apple Searhs Ads is clicked and for which campaign + guard let attribution = json["attribution"] as? Bool else { + Logger.module.error("Failed to unwrap json to Ad Attribution property.") + UrpLog.log("Failed to unwrap json to Ad Attribution property. \(json)") + + throw SerializationError.missing("Attribution Context") + } + + guard let campaignId = json["campaignId"] as? Int else { + Logger.module.error("Failed to unwrap json to Campaign Id property.") + UrpLog.log("Failed to unwrap json to Campaign Id property. \(json)") + + throw SerializationError.missing("Campaign Id") + } + + if let conversionType = json["conversionType"] as? String { + guard conversionType == "Download" || conversionType == "Redownload" else { + throw SerializationError.invalid("Conversion Type", conversionType) + } + } + + self.attribution = attribution + self.organizationId = json["orgId"] as? Int + self.conversionType = json["conversionType"] as? String + self.campaignId = campaignId + self.countryOrRegion = json["countryOrRegion"] as? String + } +} diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index 25025dc48cc..1d15af655e2 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -7,6 +7,8 @@ import BraveShared import CertificateUtilities import SwiftyJSON import os.log +import Combine +import BraveCore enum UrpError { case networkError, downloadIdNotFound, ipNotFound, endpointError @@ -21,20 +23,19 @@ struct UrpService { static let downLoadId = "download_id" } - let host: String + private let host: String + private let adServicesURL: String private let apiKey: String - let sessionManager: URLSession - private let certificateEvaluator: PinningCertificateEvaluator + private let sessionManager: URLSession + private let certificateEvaluator: URPCertificatePinningService - init?(host: String, apiKey: String) { + init?(host: String, apiKey: String, adServicesURL: String) { self.host = host self.apiKey = apiKey - - guard let hostUrl = URL(string: host), let normalizedHost = hostUrl.normalizedHost() else { return nil } + self.adServicesURL = adServicesURL // Certificate pinning - certificateEvaluator = PinningCertificateEvaluator(hosts: [normalizedHost]) - + certificateEvaluator = URPCertificatePinningService() sessionManager = URLSession(configuration: .default, delegate: certificateEvaluator, delegateQueue: .main) } @@ -76,6 +77,31 @@ struct UrpService { } } } + + @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String) async throws -> (AdAttributionData?) { + guard let endPoint = URL(string: adServicesURL) else { + Logger.module.error("AdServicesURLString can not be resolved: \(adServicesURL)") + throw URLError(.badURL) + } + + let attributionDataToken = adAttributionToken.data(using: .utf8) + + do { + let (result, _) = try await sessionManager.adServicesAttributionApiRequest(endPoint: endPoint, rawData: attributionDataToken) + UrpLog.log("Ad Attribution response: \(result)") + + if let resultData = result as? Data { + let jsonResponse = try JSONSerialization.jsonObject(with: resultData, options: []) as? [String: Any] + let adAttributionData = try AdAttributionData(json: jsonResponse) + + return adAttributionData + } + } catch { + throw error + } + + return (nil) + } func checkIfAuthorizedForGrant(with downloadId: String, completion: @escaping (Bool?, UrpError?) -> Void) { guard var endPoint = URL(string: host) else { @@ -109,9 +135,40 @@ struct UrpService { extension URLSession { /// All requests to referral api use PUT method, accept and receive json. func urpApiRequest(endPoint: URL, params: [String: String], completion: @escaping (Result) -> Void) { - - self.request(endPoint, method: .put, parameters: params, encoding: .json) { response in + request(endPoint, method: .put, parameters: params, encoding: .json) { response in completion(response) } } + + // Apple ad service attricution request requires plain text encoding with post method and passing token as rawdata + func adServicesAttributionApiRequest(endPoint: URL, rawData: Data?) async throws -> (Any, URLResponse) { + // According to attributiontoken API docs + // An error reponse can occur API call is done too quickly after receiving a valid token. + // A best practice is to initiate retries at intervals of 5 seconds, with a maximum of three attempts. + return try await Task.retry(retryCount: 3, retryDelay: 5) { + return try await self.request(endPoint, method: .post, rawData: rawData, encoding: .textPlain) + }.value + } +} + +class URPCertificatePinningService: NSObject, URLSessionDelegate { + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let serverTrust = challenge.protectionSpace.serverTrust { + let result = BraveCertificateUtility.verifyTrust(serverTrust, host: challenge.protectionSpace.host, port: challenge.protectionSpace.port) + // Cert is valid and should be pinned + if result == 0 { + return (.useCredential, URLCredential(trust: serverTrust)) + } + + // Cert is valid and should not be pinned + // Let the system handle it and we'll show an error if the system cannot validate it + if result == Int32.min { + return (.performDefaultHandling, nil) + } + } + return (.cancelAuthenticationChallenge, nil) + } + return (.performDefaultHandling, nil) + } } diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index 2483e4abb53..e6af1550fdd 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -7,6 +7,7 @@ import Shared import Preferences import WebKit import os.log +import AdServices public class UserReferralProgram { @@ -20,6 +21,8 @@ public class UserReferralProgram { static let staging = "https://laptop-updates.bravesoftware.com" static let prod = "https://laptop-updates.brave.com" } + + let adServicesURLString = "https://api-adservices.apple.com/api/v1/" // In case of network problems when looking for referrral code // we retry the call few times while the app is still alive. @@ -47,7 +50,7 @@ public class UserReferralProgram { return nil } - guard let urpService = UrpService(host: host, apiKey: apiKey) else { return nil } + guard let urpService = UrpService(host: host, apiKey: apiKey, adServicesURL: adServicesURLString) else { return nil } UrpLog.log("URP init, host: \(host)") @@ -55,7 +58,7 @@ public class UserReferralProgram { } /// Looks for referral and returns its landing page if possible. - public func referralLookup(completion: @escaping (_ refCode: String?, _ offerUrl: String?) -> Void) { + public func referralLookup(refCode: String? = nil, completion: @escaping (_ refCode: String?, _ offerUrl: String?) -> Void) { UrpLog.log("first run referral lookup") let referralBlock: (ReferralData?, UrpError?) -> Void = { [weak self] referral, error in @@ -75,7 +78,7 @@ public class UserReferralProgram { withTimeInterval: self.referralLookupRetry.retryTimeInterval, repeats: true ) { [weak self] _ in - self?.referralLookup() { refCode, offerUrl in + self?.referralLookup(refCode: refCode) { refCode, offerUrl in completion(refCode, offerUrl) } } @@ -114,7 +117,28 @@ public class UserReferralProgram { // Since ref-code method may not be repeatable (e.g. clipboard was cleared), this should be retrieved from prefs, // and not use the passed in referral code. - service.referralCodeLookup(refCode: UserReferralProgram.getReferralCode(), completion: referralBlock) + service.referralCodeLookup(refCode: refCode, completion: referralBlock) + } + + public func adCampaignLookup(completion: @escaping ((AdAttributionData)?, Error?) -> Void) { + // Fetching ad attibution token + do { + let adAttributionToken = try AAAttribution.attributionToken() + + Task { @MainActor in + do { + let result = try await service.adCampaignTokenLookupQueue(adAttributionToken: adAttributionToken) + completion(result, nil) + } catch { + Logger.module.info("Could not retrieve ad campaign attibution from ad services") + completion(nil, error) + } + } + } catch { + Logger.module.info("Couldnt fetch attribute tokens with error: \(error)") + completion(nil, error) + return + } } private func initRetryPingConnection(numberOfTimes: Int32) { diff --git a/Sources/Shared/AppConstants.swift b/Sources/Shared/AppConstants.swift index 18ffbcda1b9..96846482659 100644 --- a/Sources/Shared/AppConstants.swift +++ b/Sources/Shared/AppConstants.swift @@ -37,6 +37,19 @@ public enum AppBuildChannel: String { return "invalid" } } + + public var dauServerChannelParam: String { + switch self { + case .release: + return "release" + case .beta: + return "beta" + case .dev, .debug: + return "developer" + case .enterprise: + return "invalid" + } + } } public struct KVOConstants: Equatable { diff --git a/Tests/CertificateUtilitiesTests/CertificatePinningTest.swift b/Tests/CertificateUtilitiesTests/CertificatePinningTest.swift index beb137e1462..3cabb8d0d01 100644 --- a/Tests/CertificateUtilitiesTests/CertificatePinningTest.swift +++ b/Tests/CertificateUtilitiesTests/CertificatePinningTest.swift @@ -7,211 +7,7 @@ import BraveShared @testable import CertificateUtilities @testable import BraveCore -extension CertificatePinningTest { - private func certificate(named: String) -> SecCertificate { - let path = Bundle.module.path(forResource: named, ofType: ".cer")! - let certificateData = try! Data(contentsOf: URL(fileURLWithPath: path)) as CFData - return SecCertificateCreateWithData(nil, certificateData)! - } - - private func trust(for certificates: [SecCertificate]) -> SecTrust { - var trust: SecTrust! - SecTrustCreateWithCertificates(certificates as CFTypeRef, SecPolicyCreateBasicX509(), &trust) - return trust - } -} - class CertificatePinningTest: XCTestCase { - - private lazy var leaf = certificate(named: "leaf") - private lazy var intermediate = certificate(named: "intermediate") - private lazy var root = certificate(named: "root") - - func testPinningWithHostValidation() { - let host = "www.apple.com" - let trust = self.trust(for: [leaf, intermediate, root]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost]) - - do { - try evaluator.evaluate(trust, forHost: host) - } catch { - XCTFail("Validation failed but should have succeeded: \(error.localizedDescription)") - } - } - - func testFailPinningWithHostValidation() { - let host = "github.com" - let trust = self.trust(for: [leaf, intermediate, root]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost]) - - do { - try evaluator.evaluate(trust, forHost: host) - XCTFail("Validation succeeded but should have failed") - } catch { - - } - } - - func testExpiredCertificate() { - let leaf = certificate(named: "expired.badssl.com-leaf") - let intermediate = certificate(named: "expired.badssl.com-intermediate-ca-1") - let intermediate2 = certificate(named: "expired.badssl.com-intermediate-ca-2") - let root = certificate(named: "expired.badssl.com-root-ca") - - let host = "badssl.com" - let trust = self.trust(for: [leaf, intermediate, intermediate2, root]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost]) - - do { - try evaluator.evaluate(trust, forHost: host) - XCTFail("Validation succeeded but should have failed") - } catch { - - } - } - - func testUntrustedRoot() { - let leaf = certificate(named: "untrusted.badssl.com-leaf") - let root = certificate(named: "untrusted.badssl.com-root") - - let host = "badssl.com" - let trust = self.trust(for: [root]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost]) - - do { - try evaluator.evaluate(trust, forHost: host) - XCTFail("Validation succeeded but should have failed") - } catch { - - } - } - - func testSelfSignedRoot() { - let leaf = certificate(named: "untrusted.badssl.com-leaf") - let root = certificate(named: "self-signed.badssl.com") - - let host = "badssl.com" - let trust = self.trust(for: [root]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost]) - - do { - try evaluator.evaluate(trust, forHost: host) - XCTFail("Validation succeeded but should have failed") - } catch { - - } - } - - // As of iOS 13, it's extremely hard to :allow: a self-signed certificate! - // iOS 13 is validating the certificate against a list of CA's.. so to even use a self-signed certificate, you have to add it to the keychain/keystore as trusted.. - // Or sign it with a trusted CA.. For now this test has been disabled as we aren't using self-signed certificates anyway. - // https://support.apple.com/en-us/HT210176 - func testSelfSignedRootAllowed() { - let leaf = certificate(named: "self-signed") - - let host = "unit-test.brave.com" - let trust = self.trust(for: [leaf]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost, .anchorSpecificTrustsOnly]) - - do { - try evaluator.evaluate(trust, forHost: host) - } catch { - XCTFail("Validation failed but should have succeeded: \(error.localizedDescription)") - } - } - - // Same as :testSelfSignedRootAllowed: - func testSelfSignedRootAllowed2() { - let leaf = certificate(named: "expired.badssl.com-leaf") - let root = certificate(named: "self-signed.badssl.com") - - let host = "badssl.com" - let trust = self.trust(for: [root]) - let evaluator = PinningCertificateEvaluator(hosts: [host: leaf], options: [.default, .validateHost, .anchorSpecificTrustsOnly]) - - do { - try evaluator.evaluate(trust, forHost: host) - } catch { - XCTFail("Validation failed but should have succeeded: \(error.localizedDescription)") - } - } - - // Test whether or not exception URLs are NOT pinned! - func testLivePinningSuccess() { - let urls = PinningCertificateEvaluator.ExcludedPinningHostUrls.urls.map({ "https://\($0)" }) - - var expectations = [XCTestExpectation]() - for host in urls { - let expectation = XCTestExpectation(description: "Test Pinning Live URLs: \(host)") - expectations.append(expectation) - - guard let hostUrl = URL(string: host), - let normalizedHost = hostUrl.normalizedHost() - else { - - XCTFail("Invalid URL/Host for pinning: \(host)") - expectation.fulfill() - return - } - - let certificateEvaluator = PinningCertificateEvaluator(hosts: [normalizedHost]) - let sessionManager = URLSession(configuration: .default, delegate: certificateEvaluator, delegateQueue: .main) - - sessionManager.request(hostUrl, method: .put, parameters: ["unit-test": "unit-value"], encoding: .json) { response in - switch response { - case .success: - break - - case .failure(let error as NSError): - if error.code == NSURLErrorCancelled { - XCTFail("Invalid URL/Host for pinning: \(error.localizedDescription) for host: \(host)") - } - } - - expectation.fulfill() - }.resume() - sessionManager.finishTasksAndInvalidate() - } - - wait(for: expectations, timeout: 10.0) - } - - // Test whether or not pinning actually works on a live URL - func testLivePinningFailure() { - let urls = ["https://brave.com"] - - var managers = [URLSession]() - var expectations = [XCTestExpectation]() - for host in urls { - let expectation = XCTestExpectation(description: "Test Pinning Live URLs: \(host)") - expectations.append(expectation) - - guard let hostUrl = URL(string: host), - let normalizedHost = hostUrl.normalizedHost() - else { - - XCTFail("Invalid URL/Host for pinning: \(host)") - expectation.fulfill() - return - } - - let certificateEvaluator = PinningCertificateEvaluator(hosts: [normalizedHost]) - let sessionManager = URLSession(configuration: .default, delegate: certificateEvaluator, delegateQueue: .main) - managers.append(sessionManager) - - sessionManager.dataTask(with: hostUrl) { data, response, error in - if let error = error as NSError?, error.code == NSURLErrorCancelled { - XCTFail("Invalid URL/Host for pinning: \(error.localizedDescription) for host: \(host)") - } - - expectation.fulfill() - }.resume() - sessionManager.finishTasksAndInvalidate() - } - - wait(for: expectations, timeout: 10.0) - } - // Test whether pinning via Brave-Core works // https://github.com/brave/brave-core/blob/master/chromium_src/net/tools/transport_security_state_generator/input_file_parsers.cc func testBraveCoreLivePinningSuccess() { diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-1.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-1.cer deleted file mode 100644 index ad75f0fc541..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-1.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-2.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-2.cer deleted file mode 100644 index 7d7e8f271e4..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-intermediate-ca-2.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-leaf.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-leaf.cer deleted file mode 100644 index a82e960ee80..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-leaf.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-root-ca.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-root-ca.cer deleted file mode 100644 index 8a99c54a99f..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/expired.badssl.com-root-ca.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/self-signed.badssl.com.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/self-signed.badssl.com.cer deleted file mode 100644 index 7c6bda122e9..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/self-signed.badssl.com.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/untrusted.badssl.com-leaf.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/untrusted.badssl.com-leaf.cer deleted file mode 100644 index cec0bc3a593..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/untrusted.badssl.com-leaf.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/untrusted.badssl.com-root.cer b/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/untrusted.badssl.com-root.cer deleted file mode 100644 index d9d5f6c2458..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/expired.badssl.com/untrusted.badssl.com-root.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/intermediate.cer b/Tests/CertificateUtilitiesTests/Certificates/intermediate.cer deleted file mode 100644 index 03de1b4d521..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/intermediate.cer and /dev/null differ diff --git a/Tests/CertificateUtilitiesTests/Certificates/root.cer b/Tests/CertificateUtilitiesTests/Certificates/root.cer deleted file mode 100644 index dae0196507d..00000000000 Binary files a/Tests/CertificateUtilitiesTests/Certificates/root.cer and /dev/null differ diff --git a/Tests/GrowthTests/DAUTests.swift b/Tests/GrowthTests/DAUTests.swift index 5e4cf17f043..28fd5dd491b 100644 --- a/Tests/GrowthTests/DAUTests.swift +++ b/Tests/GrowthTests/DAUTests.swift @@ -39,7 +39,7 @@ class DAUTests: XCTestCase { let devExpected = URLQueryItem(name: "channel", value: "developer") XCTAssertEqual(dau.channelParam(for: .dev), devExpected) - let debugExpected = URLQueryItem(name: "channel", value: "invalid") + let debugExpected = URLQueryItem(name: "channel", value: "developer") XCTAssertEqual(dau.channelParam(for: .debug), debugExpected) let enterpriseExpected = URLQueryItem(name: "channel", value: "invalid") @@ -322,12 +322,6 @@ class DAUTests: XCTestCase { XCTAssertEqual(singleDigitTest.mondayOfCurrentWeekFormatted, "2019-02-04") } - func testNoPingOnDevelopmentBuild() { - XCTAssertTrue(AppConstants.buildChannel == .debug) - - let dau = DAU() - XCTAssertFalse(dau.sendPingToServer()) - } func testMigratingInvalidWeekOfInstallPref() throws { // (stored, fixed)