From 98aa6b528c2289ae72dc995c3bd519df967ff3f2 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Wed, 6 Dec 2023 16:56:50 -0500 Subject: [PATCH 01/20] Moving DAU logic to separate file outside scene delegate --- App/iOS/Delegates/SceneDelegate.swift | 55 +--------------- .../BVC+Attribution.swift | 63 +++++++++++++++++++ ...er+BraveTalk.swift => BVC+BraveTalk.swift} | 0 ...roller+Callout.swift => BVC+Callout.swift} | 0 ....swift => BVC+DownloadQueueDelegate.swift} | 0 ...ate.swift => BVC+FindInPageDelegate.swift} | 0 ...+IPFSScheme.swift => BVC+IPFSScheme.swift} | 0 ...eyCommands.swift => BVC+KeyCommands.swift} | 0 ...ller+Keyboard.swift => BVC+Keyboard.swift} | 0 ...ewController+Menu.swift => BVC+Menu.swift} | 0 ...+Onboarding.swift => BVC+Onboarding.swift} | 0 ...+OpenSearch.swift => BVC+OpenSearch.swift} | 0 ...ViewController+P3A.swift => BVC+P3A.swift} | 0 ...on.swift => BVC+ProductNotification.swift} | 0 ...+ReaderMode.swift => BVC+ReaderMode.swift} | 0 ...ate.swift => BVC+TabManagerDelegate.swift} | 0 ...legate.swift => BVC+ToolbarDelegate.swift} | 0 ...ft => BVC+UIDropInteractionDelegate.swift} | 0 ...e.swift => BVC+WKNavigationDelegate.swift} | 0 ...ntroller+Wallet.swift => BVC+Wallet.swift} | 0 ...ervice.swift => BVC+Web3NameService.swift} | 0 ...roller+Widgets.swift => BVC+Widgets.swift} | 0 .../BrowserViewController.swift | 3 + .../OpenSearchEngineButton.swift | 1 + 24 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+BraveTalk.swift => BVC+BraveTalk.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Callout.swift => BVC+Callout.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+DownloadQueueDelegate.swift => BVC+DownloadQueueDelegate.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+FindInPageDelegate.swift => BVC+FindInPageDelegate.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+IPFSScheme.swift => BVC+IPFSScheme.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+KeyCommands.swift => BVC+KeyCommands.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Keyboard.swift => BVC+Keyboard.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Menu.swift => BVC+Menu.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Onboarding.swift => BVC+Onboarding.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{OpenSearch/BrowserViewController+OpenSearch.swift => BVC+OpenSearch.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+P3A.swift => BVC+P3A.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+ProductNotification.swift => BVC+ProductNotification.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+ReaderMode.swift => BVC+ReaderMode.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+TabManagerDelegate.swift => BVC+TabManagerDelegate.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+ToolbarDelegate.swift => BVC+ToolbarDelegate.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+UIDropInteractionDelegate.swift => BVC+UIDropInteractionDelegate.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+WKNavigationDelegate.swift => BVC+WKNavigationDelegate.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Wallet.swift => BVC+Wallet.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Web3NameService.swift => BVC+Web3NameService.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+Widgets.swift => BVC+Widgets.swift} (100%) rename Sources/Brave/Frontend/Browser/{ => BrowserViewController}/BrowserViewController.swift (99%) rename Sources/Brave/Frontend/Browser/{BrowserViewController/OpenSearch => Search}/OpenSearchEngineButton.swift (99%) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 1d5de04ac76..a3ed07bbb86 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -44,6 +44,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { scene: windowScene, braveCore: AppState.shared.braveCore, profile: AppState.shared.profile, + dau: AppState.shared.dau, diskImageStore: AppState.shared.diskImageStore, migration: AppState.shared.migration, rewards: AppState.shared.rewards, @@ -422,6 +423,7 @@ extension SceneDelegate { private func createBrowserWindow(scene: UIWindowScene, braveCore: BraveCoreMain, profile: Profile, + dau: DAU, diskImageStore: DiskImageStore?, migration: Migration?, rewards: Brave.BraveRewards, @@ -480,6 +482,7 @@ extension SceneDelegate { let browserViewController = BrowserViewController( windowId: windowId, profile: profile, + dau: dau, diskImageStore: diskImageStore, braveCore: braveCore, rewards: rewards, @@ -574,58 +577,6 @@ extension SceneDelegate: UIViewControllerRestoration { } } -extension BrowserViewController { - func handleReferralLookup(_ urp: UserReferralProgram) { - if Preferences.URP.referralLookupOutstanding.value == true { - 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 { /// A single scene should only have ONE browserViewController /// However, it is possible that someone can create multiple, diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift new file mode 100644 index 00000000000..b6411232f93 --- /dev/null +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift @@ -0,0 +1,63 @@ +// 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 Foundation +import Preferences +import Growth +import Shared + +extension BrowserViewController { + public func handleReferralLookup(_ urp: UserReferralProgram) { + if Preferences.URP.referralLookupOutstanding.value == true { + performProgramReferralLookup(urp, refCode: UserReferralProgram.getReferralCode()) + } else { + urp.pingIfEnoughTimePassed() + } + } + + public 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) { [weak self] referralCode, offerUrl in + guard let self = self else { return } + + // 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 { + self.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) + } + } +} diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+BraveTalk.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+BraveTalk.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+BraveTalk.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+BraveTalk.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+DownloadQueueDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+DownloadQueueDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+DownloadQueueDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+DownloadQueueDelegate.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+FindInPageDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+FindInPageDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+FindInPageDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+FindInPageDelegate.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+IPFSScheme.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+IPFSScheme.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+IPFSScheme.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+IPFSScheme.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+KeyCommands.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+KeyCommands.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Keyboard.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Keyboard.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Keyboard.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Keyboard.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/OpenSearch/BrowserViewController+OpenSearch.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+OpenSearch.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/OpenSearch/BrowserViewController+OpenSearch.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+OpenSearch.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+P3A.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+P3A.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+P3A.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+P3A.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ProductNotification.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ProductNotification.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ReaderMode.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ReaderMode.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ReaderMode.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ReaderMode.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+UIDropInteractionDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+UIDropInteractionDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+UIDropInteractionDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+UIDropInteractionDelegate.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Wallet.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Wallet.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Web3NameService.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Web3NameService.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Web3NameService.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Web3NameService.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Widgets.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Widgets.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Widgets.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Widgets.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift similarity index 99% rename from Sources/Brave/Frontend/Browser/BrowserViewController.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index aad493e6677..43c46ae83fe 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -162,6 +162,7 @@ public class BrowserViewController: UIViewController { public let windowId: UUID let profile: Profile + let dau: DAU let braveCore: BraveCoreMain let tabManager: TabManager let migration: Migration? @@ -272,6 +273,7 @@ public class BrowserViewController: UIViewController { public init( windowId: UUID, profile: Profile, + dau: DAU, diskImageStore: DiskImageStore?, braveCore: BraveCoreMain, rewards: BraveRewards, @@ -282,6 +284,7 @@ public class BrowserViewController: UIViewController { ) { self.windowId = windowId self.profile = profile + self.dau = dau self.braveCore = braveCore self.bookmarkManager = BookmarkManager(bookmarksAPI: braveCore.bookmarksAPI) self.rewards = rewards diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/OpenSearch/OpenSearchEngineButton.swift b/Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift similarity index 99% rename from Sources/Brave/Frontend/Browser/BrowserViewController/OpenSearch/OpenSearchEngineButton.swift rename to Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift index d68b8539a0f..a737c8407e9 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/OpenSearch/OpenSearchEngineButton.swift +++ b/Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift @@ -78,3 +78,4 @@ class OpenSearchEngineButton: BraveButton { } } } + From 6c4b1d355dad63a981982217a5983a7151cc0e37 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Thu, 7 Dec 2023 17:52:15 -0500 Subject: [PATCH 02/20] Handling server ping app first launch properly --- App/iOS/Delegates/AppDelegate.swift | 16 ++++++++-------- App/iOS/Delegates/SceneDelegate.swift | 2 +- .../BrowserViewController/BVC+Attribution.swift | 11 +++++------ Sources/Growth/DAU.swift | 2 +- Sources/Growth/URP/UserReferralProgram.swift | 3 ++- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index af821bda8e1..1d7c3332f51 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -213,18 +213,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } SceneDelegate.shouldHandleUrpLookup = true + + 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 } else { log.error("Failed to initialize user referral program") 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( diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index a3ed07bbb86..5018fad1ff2 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -213,7 +213,7 @@ 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 and install attribution has processed. - if Preferences.URP.referralLookupOutstanding.value == false, Preferences.URP.installAttributionLookupOutstanding.value == false { + if Preferences.URP.referralLookupOutstanding.value == true, Preferences.URP.installAttributionLookupOutstanding.value == true { AppState.shared.dau.sendPingToServer() } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift index b6411232f93..9cd723793d1 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift @@ -26,6 +26,8 @@ extension BrowserViewController { // This value should be set before first DAU ping Preferences.URP.referralCode.value = refCode Preferences.URP.installAttributionLookupOutstanding.value = false + + self.dau.sendPingToServer() } } @@ -46,15 +48,12 @@ extension BrowserViewController { urp.referralLookup(refCode: refCode) { [weak self] referralCode, offerUrl in guard let self = self else { return } - // 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 { - self.dau.sendPingToServer() - } + Preferences.URP.referralLookupOutstanding.value = false + let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes let retryDeadline = Date() + retryTime - Preferences.NewTabPage.superReferrerThemeRetryDeadline.value = retryDeadline + Preferences.NewTabPage.superReferrerThemeRetryDeadline.value = retryDeadline guard let url = offerUrl?.asURL else { return } self.openReferralLink(url: url) diff --git a/Sources/Growth/DAU.swift b/Sources/Growth/DAU.swift index c697c437873..0a40e461c93 100644 --- a/Sources/Growth/DAU.swift +++ b/Sources/Growth/DAU.swift @@ -89,7 +89,7 @@ public class DAU { return true } - @objc public func sendPingToServerInternal() { + @objc private func sendPingToServerInternal() { guard let paramsAndPrefs = paramsAndPrefsSetup(for: Date()) else { Logger.module.debug("dau, no changes detected, no server ping") UrpLog.log("dau, no changes detected, no server ping") diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index e6af1550fdd..f500fde7420 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -229,7 +229,8 @@ public class UserReferralProgram { UrpLog.log("Enough time has passed, removing referral code data") return nil } else if let referralCode = Preferences.URP.referralCode.value { - // Appending ref code to dau ping if user used installed the app via user referral program. + // Appending ref code to dau ping if user used installed the app via + // user referral program or apple search ad if Preferences.URP.referralCodeDeleteDate.value == nil { UrpLog.log("Setting new date for deleting referral code.") let timeToDelete = AppConstants.buildChannel.isPublic ? 90.days : 20.minutes From 28649f7e98762e3be091f0b5871273c3918bfa6a Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Mon, 11 Dec 2023 15:33:51 -0500 Subject: [PATCH 03/20] Adding daily user ping user p3a consent terms --- App/iOS/Delegates/AppDelegate.swift | 1 + App/iOS/Delegates/SceneDelegate.swift | 13 +++++++++++ .../BVC+Attribution.swift | 23 +++++++++---------- .../Brave/Frontend/ClientPreferences.swift | 4 ---- Sources/Growth/DAU.swift | 3 +++ Sources/Preferences/GlobalPreferences.swift | 5 ++++ 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index 1d7c3332f51..fd6ea49acd0 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -150,6 +150,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let isFirstLaunch = Preferences.General.isFirstLaunch.value Preferences.AppState.isOnboardingActive.value = isFirstLaunch + Preferences.AppState.dailyUserPingAwaitingUserConsent.value = isFirstLaunch if Preferences.Onboarding.basicOnboardingCompleted.value == OnboardingState.undetermined.rawValue { Preferences.Onboarding.basicOnboardingCompleted.value = diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 5018fad1ff2..8ff0bbbd700 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -99,6 +99,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Handle Install Attribution Fetch at first launch if SceneDelegate.shouldHandleInstallAttributionFetch { SceneDelegate.shouldHandleInstallAttributionFetch = false + + // First time user should send dau ping after onboarding last stage _ p3a consent screen + // The reason p3a user consent is necesserray to call search ad install attribution API methods + if Preferences.AppState.dailyUserPingAwaitingUserConsent.value { + return + } + + // If P3A is not enabled, send the organic install code at daily pings which is BRV001 + // User has not opted in to share completely private and anonymous product insights + guard AppState.shared.braveCore.p3aUtils.isP3AEnabled else { + browserViewController.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) + return + } if let urp = UserReferralProgram.shared { browserViewController.handleSearchAdsInstallAttribution(urp) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift index 9cd723793d1..1e6bd3cc5f4 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift @@ -22,18 +22,13 @@ extension BrowserViewController { 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 - - self.dau.sendPingToServer() + self.setupReferralCodeAndPingServer(refCode: refCode) } } private func generateReferralCode(attributionData: AdAttributionData?, fetchError: Error?) -> String { // Prefix code "001" with BRV for organic iOS installs - var referralCode = "BRV001" + var referralCode = DAU.organicInstallReferralCode if fetchError == nil, attributionData?.attribution == true, let campaignId = attributionData?.campaignId { // Adding ASA User refcode prefix to indicate @@ -44,17 +39,21 @@ extension BrowserViewController { return referralCode } + public func setupReferralCodeAndPingServer(refCode: String) { + // Setting up referral code value + // This value should be set before first DAU ping + Preferences.URP.referralCode.value = refCode + Preferences.URP.installAttributionLookupOutstanding.value = false + + dau.sendPingToServer() + } + private func performProgramReferralLookup(_ urp: UserReferralProgram, refCode: String?) { urp.referralLookup(refCode: refCode) { [weak self] referralCode, offerUrl in guard let self = self else { return } Preferences.URP.referralLookupOutstanding.value = false - 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) } diff --git a/Sources/Brave/Frontend/ClientPreferences.swift b/Sources/Brave/Frontend/ClientPreferences.swift index fff74b178f1..bf0970b379c 100644 --- a/Sources/Brave/Frontend/ClientPreferences.swift +++ b/Sources/Brave/Frontend/ClientPreferences.swift @@ -174,10 +174,6 @@ extension Preferences { /// List of currently installed themes on the device. static let installedCustomThemes = Option<[String]>(key: "newtabpage.installed-custom-themes", default: []) - - /// Tells the app whether we should try to fetch super referrer assets again in case of network error. - public static let superReferrerThemeRetryDeadline = - Option(key: "newtabpage.superreferrer-retry-deadline", default: nil) /// Tells the app whether we should show Privacy Hub in new tab page view controller public static let showNewTabPrivacyHub = diff --git a/Sources/Growth/DAU.swift b/Sources/Growth/DAU.swift index 0a40e461c93..fbcfc243814 100644 --- a/Sources/Growth/DAU.swift +++ b/Sources/Growth/DAU.swift @@ -25,6 +25,9 @@ public class DAU { /// Number of seconds that determins when a user is "active" private let pingRefreshDuration = 5.minutes + /// The default DAU referral code + public static let organicInstallReferralCode = "BRV001" + /// We always use gregorian calendar for DAU pings. This also adds more anonymity to the server call. fileprivate static var calendar: Calendar { var cal = Calendar(identifier: .gregorian) diff --git a/Sources/Preferences/GlobalPreferences.swift b/Sources/Preferences/GlobalPreferences.swift index 36838f7916b..cc0a54bcec3 100644 --- a/Sources/Preferences/GlobalPreferences.swift +++ b/Sources/Preferences/GlobalPreferences.swift @@ -140,6 +140,11 @@ extension Preferences { /// /// This is used to determine if a promoted purchase from store can be triggered and shown user public static let isOnboardingActive = Option(key: "appstate.onboarding-active", default: false) + + /// A cached value for indicating if dau ping is awaiting for p3a choice on onboarding + /// + /// This is used to determine if a user gave consent to p3a in onboarding and dau ping can fetch referral code from Apple API + public static let dailyUserPingAwaitingUserConsent = Option(key: "appstate.dau-awaiting", default: false) } public final class Chromium { From 4601c594cccf00187ddedd7c8fda59aacb5e5936 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Wed, 13 Dec 2023 12:27:30 -0500 Subject: [PATCH 04/20] Adding trigger loading at last stage of onboarding on button consent --- App/iOS/Delegates/SceneDelegate.swift | 21 ++++++++--------- .../Welcome/WelcomeViewCallout.swift | 23 ++++++++++++++++--- .../Welcome/WelcomeViewController.swift | 13 +++++++++-- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 8ff0bbbd700..fa5a0400a61 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -103,18 +103,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // First time user should send dau ping after onboarding last stage _ p3a consent screen // The reason p3a user consent is necesserray to call search ad install attribution API methods if Preferences.AppState.dailyUserPingAwaitingUserConsent.value { - return - } - - // If P3A is not enabled, send the organic install code at daily pings which is BRV001 - // User has not opted in to share completely private and anonymous product insights - guard AppState.shared.braveCore.p3aUtils.isP3AEnabled else { - browserViewController.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) - return - } - - if let urp = UserReferralProgram.shared { - browserViewController.handleSearchAdsInstallAttribution(urp) + // If P3A is not enabled, send the organic install code at daily pings which is BRV001 + // User has not opted in to share completely private and anonymous product insights + if AppState.shared.braveCore.p3aUtils.isP3AEnabled { + if let urp = UserReferralProgram.shared { + browserViewController.handleSearchAdsInstallAttribution(urp) + } + } else { + browserViewController.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) + } } } diff --git a/Sources/Onboarding/Welcome/WelcomeViewCallout.swift b/Sources/Onboarding/Welcome/WelcomeViewCallout.swift index 6c3af0d4272..84619648ef7 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewCallout.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewCallout.swift @@ -124,8 +124,15 @@ class WelcomeViewCallout: UIView { } private let primaryButton = RoundInterfaceButton(type: .custom).then { - $0.setTitleColor(.white, for: .normal) - $0.backgroundColor = .braveBlurpleTint + $0.configuration = .filled() + $0.configuration?.showsActivityIndicator = false + $0.configuration?.imagePadding = 5 + $0.configuration?.activityIndicatorColorTransformer = UIConfigurationColorTransformer({ _ in + .white + }) + $0.configuration?.baseForegroundColor = .white + $0.configuration?.baseBackgroundColor = .braveBlurpleTint + $0.titleLabel?.numberOfLines = 0 $0.titleLabel?.minimumScaleFactor = 0.7 $0.titleLabel?.adjustsFontSizeToFitWidth = true @@ -169,6 +176,12 @@ class WelcomeViewCallout: UIView { arrowView.isHidden = isBottomArrowHidden } } + + var isLoading = false { + didSet { + primaryButton.setNeedsUpdateConfiguration() + } + } init() { super.init(frame: .zero) @@ -488,7 +501,7 @@ class WelcomeViewCallout: UIView { } primaryButton.do { - $0.setTitle(info.primaryButtonTitle, for: .normal) + $0.configuration?.title = info.primaryButtonTitle $0.titleLabel?.font = .preferredFont(for: .body, weight: .regular) $0.addAction( UIAction( @@ -498,6 +511,10 @@ class WelcomeViewCallout: UIView { }), for: .touchUpInside) $0.alpha = 1.0 $0.isHidden = false + $0.configurationUpdateHandler = { button in + button.configuration?.title = self.isLoading ? "" : info.primaryButtonTitle + button.configuration?.showsActivityIndicator = self.isLoading + } } secondaryLabel.do { diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index bfab1d61fbf..826c0dbe86f 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -375,8 +375,17 @@ public class WelcomeViewController: UIViewController { nextController.present(p3aLearnMoreController, animated: true) }, - primaryButtonAction: { [weak self] in - self?.close() + primaryButtonAction: { [weak nextController, weak self] in + guard nextController?.calloutView.isLoading == false else { + return + } + + nextController?.calloutView.isLoading = true + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { + nextController?.calloutView.isLoading = false + self?.close() + } } ) ) From 4609c17acadf87b185b57629ca1f046a606ba02a Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 15 Dec 2023 17:04:13 -0500 Subject: [PATCH 05/20] Adding welcome controller means to run dau referral call API --- .../BrowserViewController/BVC+Callout.swift | 4 +++- .../BrowserViewController/BVC+Menu.swift | 1 + .../BVC+Onboarding.swift | 2 +- ...onPreferencesDebugMenuViewController.swift | 8 +++++-- .../Settings/SettingsViewController.swift | 5 ++++- .../Welcome/WelcomeViewController.swift | 21 ++++++++++++------- 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift index 0099d21da3a..dcf3b08ec49 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift @@ -155,7 +155,9 @@ extension BrowserViewController { self?.dismiss(animated: false) } ) - ), p3aUtilities: braveCore.p3aUtils + ), + p3aUtilities: braveCore.p3aUtils, + dau: dau ) present(onboardingController, animated: true) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift index 73fc4630d9f..5f73ad53620 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift @@ -187,6 +187,7 @@ extension BrowserViewController { rewards: self.rewards, windowProtection: self.windowProtection, braveCore: self.braveCore, + dau: dau, keyringStore: keyringStore, cryptoStore: cryptoStore ) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift index ff8c9939ead..cdf8a679bff 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift @@ -38,7 +38,7 @@ extension BrowserViewController { // 2. User hasn't completed onboarding if Preferences.Onboarding.basicOnboardingCompleted.value != OnboardingState.completed.rawValue, Preferences.Onboarding.isNewRetentionUser.value == true { - let onboardingController = WelcomeViewController(p3aUtilities: braveCore.p3aUtils) + let onboardingController = WelcomeViewController(p3aUtilities: braveCore.p3aUtils, dau: dau) onboardingController.modalPresentationStyle = .fullScreen parentController.present(onboardingController, animated: false) isOnboardingOrFullScreenCalloutPresented = true diff --git a/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift b/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift index 7ebbea1d8c6..78c4a748804 100644 --- a/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift +++ b/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift @@ -11,12 +11,16 @@ import UIKit import BraveUI import Onboarding import BraveCore +import Growth class RetentionPreferencesDebugMenuViewController: TableViewController { private let p3aUtilities: BraveP3AUtils + private let dau: DAU - init(p3aUtilities: BraveP3AUtils) { + init(p3aUtilities: BraveP3AUtils, dau: DAU) { self.p3aUtilities = p3aUtilities + self.dau = dau + super.init(style: .insetGrouped) } @@ -55,7 +59,7 @@ class RetentionPreferencesDebugMenuViewController: TableViewController { .init( text: "Start Onboarding", selection: { [unowned self] in - let onboardingController = WelcomeViewController(state: .loading, p3aUtilities: self.p3aUtilities) + let onboardingController = WelcomeViewController(state: .loading, p3aUtilities: self.p3aUtilities, dau: dau) onboardingController.modalPresentationStyle = .fullScreen present(onboardingController, animated: false) diff --git a/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/Sources/Brave/Frontend/Settings/SettingsViewController.swift index b0c8a8f2711..6be3170a455 100644 --- a/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -58,6 +58,7 @@ class SettingsViewController: TableViewController { private let syncAPI: BraveSyncAPI private let syncProfileServices: BraveSyncProfileServiceIOS private let p3aUtilities: BraveP3AUtils + private let dau: DAU private let keyringStore: KeyringStore? private let cryptoStore: CryptoStore? private let windowProtection: WindowProtection? @@ -73,6 +74,7 @@ class SettingsViewController: TableViewController { rewards: BraveRewards? = nil, windowProtection: WindowProtection?, braveCore: BraveCoreMain, + dau: DAU, keyringStore: KeyringStore? = nil, cryptoStore: CryptoStore? = nil ) { @@ -86,6 +88,7 @@ class SettingsViewController: TableViewController { self.syncAPI = braveCore.syncAPI self.syncProfileServices = braveCore.syncProfileService self.p3aUtilities = braveCore.p3aUtils + self.dau = dau self.keyringStore = keyringStore self.cryptoStore = cryptoStore self.ipfsAPI = braveCore.ipfsAPI @@ -861,7 +864,7 @@ class SettingsViewController: TableViewController { Row( text: "Retention Preferences Debug Menu", selection: { [unowned self] in - self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities), animated: true) + self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities, dau: dau), animated: true) }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row( text: "Load all QA Links", diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 826c0dbe86f..0cd2761e491 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -13,6 +13,7 @@ import BraveCore import BraveUI import SafariServices import DesignSystem +import Growth private enum WelcomeViewID: Int { case background = 1 @@ -27,15 +28,19 @@ private enum WelcomeViewID: Int { public class WelcomeViewController: UIViewController { private var state: WelcomeViewCalloutState? - private let p3aUtilities: BraveP3AUtils + private let p3aUtilities: BraveP3AUtils // Privacy Analytics + private let dau: DAU // Daily Active User + private let urp: UserReferralProgram? // User Referral which in sync with dau - public convenience init(p3aUtilities: BraveP3AUtils) { - self.init(state: .loading, p3aUtilities: p3aUtilities) + public convenience init(p3aUtilities: BraveP3AUtils, dau: DAU) { + self.init(state: .loading, p3aUtilities: p3aUtilities, dau: dau) } - public init(state: WelcomeViewCalloutState?, p3aUtilities: BraveP3AUtils) { + public init(state: WelcomeViewCalloutState?, p3aUtilities: BraveP3AUtils, dau: DAU) { self.state = state self.p3aUtilities = p3aUtilities + self.dau = dau + self.urp = UserReferralProgram.shared super.init(nibName: nil, bundle: nil) self.transitioningDelegate = self @@ -316,14 +321,14 @@ public class WelcomeViewController: UIViewController { } private func animateToWelcomeState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: self.dau).then { $0.setLayoutState(state: WelcomeViewCalloutState.welcome(title: Strings.Onboarding.welcomeScreenTitle)) } present(nextController, animated: true) } private func animateToDefaultBrowserState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities) + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: self.dau) let state = WelcomeViewCalloutState.defaultBrowser( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.defaultBrowserCalloutTitle, @@ -344,7 +349,7 @@ public class WelcomeViewController: UIViewController { } private func animateToDefaultSettingsState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: self.dau).then { $0.setLayoutState( state: WelcomeViewCalloutState.settings( title: Strings.Onboarding.navigateSettingsOnboardingScreenTitle, @@ -357,7 +362,7 @@ public class WelcomeViewController: UIViewController { } private func animateToP3aState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities) + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: dau) let state = WelcomeViewCalloutState.p3a( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.p3aCalloutTitle, From 8519a0db234c239127697ee05d45d7cc331cd70d Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Mon, 18 Dec 2023 17:47:35 -0500 Subject: [PATCH 06/20] Search Attribution Ad async api call --- App/iOS/Delegates/SceneDelegate.swift | 8 ++++- .../BVC+Attribution.swift | 26 ++++----------- Sources/Growth/URP/UserReferralProgram.swift | 32 ++++++++++++------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index fa5a0400a61..327646f9094 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -107,7 +107,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // User has not opted in to share completely private and anonymous product insights if AppState.shared.braveCore.p3aUtils.isP3AEnabled { if let urp = UserReferralProgram.shared { - browserViewController.handleSearchAdsInstallAttribution(urp) + Task { @MainActor in + do { + try await browserViewController.handleSearchAdsInstallAttribution(urp) + } catch { + Logger.module.debug("Error fetching ads attribution \(error)") + } + } } } else { browserViewController.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift index 1e6bd3cc5f4..ea848064865 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift @@ -17,28 +17,16 @@ extension BrowserViewController { } } - public 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) - self.setupReferralCodeAndPingServer(refCode: refCode) + @MainActor public func handleSearchAdsInstallAttribution(_ urp: UserReferralProgram) async throws { + do { + let attributionData = try await urp.adCampaignLookup() + let refCode = urp.generateReferralCode(attributionData: attributionData) + setupReferralCodeAndPingServer(refCode: refCode) + } catch { + throw error } } - private func generateReferralCode(attributionData: AdAttributionData?, fetchError: Error?) -> String { - // Prefix code "001" with BRV for organic iOS installs - var referralCode = DAU.organicInstallReferralCode - - 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 - } - public func setupReferralCodeAndPingServer(refCode: String) { // Setting up referral code value // This value should be set before first DAU ping diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index f500fde7420..f887f1622d6 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -120,25 +120,35 @@ public class UserReferralProgram { service.referralCodeLookup(refCode: refCode, completion: referralBlock) } - public func adCampaignLookup(completion: @escaping ((AdAttributionData)?, Error?) -> Void) { + @MainActor public func adCampaignLookup() async throws -> AdAttributionData? { // 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) - } + do { + return try await service.adCampaignTokenLookupQueue(adAttributionToken: adAttributionToken) + + } catch { + Logger.module.info("Could not retrieve ad campaign attibution from ad services") + throw error } } catch { Logger.module.info("Couldnt fetch attribute tokens with error: \(error)") - completion(nil, error) - return + throw error + } + } + + public func generateReferralCode(attributionData: AdAttributionData?) -> String { + // Prefix code "001" with BRV for organic iOS installs + var referralCode = DAU.organicInstallReferralCode + + if 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 initRetryPingConnection(numberOfTimes: Int32) { From 59fd4c51a42e3fe5b62c01d52f8732ac0f010841 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Tue, 19 Dec 2023 13:57:24 -0500 Subject: [PATCH 07/20] AttributionManager was created so daily active user and user referral logic can be decoupled --- App/iOS/Delegates/SceneDelegate.swift | 29 ++++++++++--------- .../BrowserViewController/BVC+Callout.swift | 2 +- .../BrowserViewController/BVC+Menu.swift | 2 +- .../BVC+Onboarding.swift | 2 +- .../BrowserViewController.swift | 6 ++-- ...onPreferencesDebugMenuViewController.swift | 8 ++--- .../Settings/SettingsViewController.swift | 8 ++--- .../URP/AttributionManager.swift} | 29 ++++++++++++------- Sources/Growth/URP/UrpService.swift | 2 +- Sources/Growth/URP/UserReferralProgram.swift | 14 +++------ .../Welcome/WelcomeViewController.swift | 20 ++++++------- 11 files changed, 61 insertions(+), 61 deletions(-) rename Sources/{Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift => Growth/URP/AttributionManager.swift} (59%) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 327646f9094..4d067b3bfb3 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -40,11 +40,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } + let attributionManager = AttributionManager(dau: AppState.shared.dau, urp: UserReferralProgram.shared) + let browserViewController = createBrowserWindow( scene: windowScene, braveCore: AppState.shared.braveCore, profile: AppState.shared.profile, - dau: AppState.shared.dau, + attributionManager: attributionManager, diskImageStore: AppState.shared.diskImageStore, migration: AppState.shared.migration, rewards: AppState.shared.rewards, @@ -90,9 +92,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Handle URP Lookup at first launch if SceneDelegate.shouldHandleUrpLookup { SceneDelegate.shouldHandleUrpLookup = false - - if let urp = UserReferralProgram.shared { - browserViewController.handleReferralLookup(urp) + + attributionManager.handleReferralLookup { [weak browserViewController] url in + browserViewController?.openReferralLink(url: url) } } @@ -106,17 +108,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // If P3A is not enabled, send the organic install code at daily pings which is BRV001 // User has not opted in to share completely private and anonymous product insights if AppState.shared.braveCore.p3aUtils.isP3AEnabled { - if let urp = UserReferralProgram.shared { - Task { @MainActor in - do { - try await browserViewController.handleSearchAdsInstallAttribution(urp) - } catch { - Logger.module.debug("Error fetching ads attribution \(error)") - } + Task { @MainActor in + do { + try await attributionManager.handleSearchAdsInstallAttribution() + } catch { + Logger.module.debug("Error fetching ads attribution default code is sent \(error)") + attributionManager.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) } } } else { - browserViewController.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) + attributionManager.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) } } } @@ -439,7 +440,7 @@ extension SceneDelegate { private func createBrowserWindow(scene: UIWindowScene, braveCore: BraveCoreMain, profile: Profile, - dau: DAU, + attributionManager: AttributionManager, diskImageStore: DiskImageStore?, migration: Migration?, rewards: Brave.BraveRewards, @@ -498,7 +499,7 @@ extension SceneDelegate { let browserViewController = BrowserViewController( windowId: windowId, profile: profile, - dau: dau, + attributionManager: attributionManager, diskImageStore: diskImageStore, braveCore: braveCore, rewards: rewards, diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift index dcf3b08ec49..4530e6d7b3f 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Callout.swift @@ -157,7 +157,7 @@ extension BrowserViewController { ) ), p3aUtilities: braveCore.p3aUtils, - dau: dau + attributionManager: attributionManager ) present(onboardingController, animated: true) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift index 5f73ad53620..c5691c39e3b 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift @@ -187,7 +187,7 @@ extension BrowserViewController { rewards: self.rewards, windowProtection: self.windowProtection, braveCore: self.braveCore, - dau: dau, + attributionManager: attributionManager, keyringStore: keyringStore, cryptoStore: cryptoStore ) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift index cdf8a679bff..7b0ffba8430 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Onboarding.swift @@ -38,7 +38,7 @@ extension BrowserViewController { // 2. User hasn't completed onboarding if Preferences.Onboarding.basicOnboardingCompleted.value != OnboardingState.completed.rawValue, Preferences.Onboarding.isNewRetentionUser.value == true { - let onboardingController = WelcomeViewController(p3aUtilities: braveCore.p3aUtils, dau: dau) + let onboardingController = WelcomeViewController(p3aUtilities: braveCore.p3aUtils, attributionManager: attributionManager) onboardingController.modalPresentationStyle = .fullScreen parentController.present(onboardingController, animated: false) isOnboardingOrFullScreenCalloutPresented = true diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index 43c46ae83fe..ccbb7a87ebe 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -162,7 +162,7 @@ public class BrowserViewController: UIViewController { public let windowId: UUID let profile: Profile - let dau: DAU + let attributionManager: AttributionManager let braveCore: BraveCoreMain let tabManager: TabManager let migration: Migration? @@ -273,7 +273,7 @@ public class BrowserViewController: UIViewController { public init( windowId: UUID, profile: Profile, - dau: DAU, + attributionManager: AttributionManager, diskImageStore: DiskImageStore?, braveCore: BraveCoreMain, rewards: BraveRewards, @@ -284,7 +284,7 @@ public class BrowserViewController: UIViewController { ) { self.windowId = windowId self.profile = profile - self.dau = dau + self.attributionManager = attributionManager self.braveCore = braveCore self.bookmarkManager = BookmarkManager(bookmarksAPI: braveCore.bookmarksAPI) self.rewards = rewards diff --git a/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift b/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift index 78c4a748804..36178fcd43d 100644 --- a/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift +++ b/Sources/Brave/Frontend/Settings/Debug/RetentionPreferencesDebugMenuViewController.swift @@ -15,11 +15,11 @@ import Growth class RetentionPreferencesDebugMenuViewController: TableViewController { private let p3aUtilities: BraveP3AUtils - private let dau: DAU + private let attributionManager: AttributionManager - init(p3aUtilities: BraveP3AUtils, dau: DAU) { + init(p3aUtilities: BraveP3AUtils, attributionManager: AttributionManager) { self.p3aUtilities = p3aUtilities - self.dau = dau + self.attributionManager = attributionManager super.init(style: .insetGrouped) } @@ -59,7 +59,7 @@ class RetentionPreferencesDebugMenuViewController: TableViewController { .init( text: "Start Onboarding", selection: { [unowned self] in - let onboardingController = WelcomeViewController(state: .loading, p3aUtilities: self.p3aUtilities, dau: dau) + let onboardingController = WelcomeViewController(state: .loading, p3aUtilities: self.p3aUtilities, attributionManager: attributionManager) onboardingController.modalPresentationStyle = .fullScreen present(onboardingController, animated: false) diff --git a/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/Sources/Brave/Frontend/Settings/SettingsViewController.swift index 6be3170a455..8f145b4ec29 100644 --- a/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -58,7 +58,7 @@ class SettingsViewController: TableViewController { private let syncAPI: BraveSyncAPI private let syncProfileServices: BraveSyncProfileServiceIOS private let p3aUtilities: BraveP3AUtils - private let dau: DAU + private let attributionManager: AttributionManager private let keyringStore: KeyringStore? private let cryptoStore: CryptoStore? private let windowProtection: WindowProtection? @@ -74,7 +74,7 @@ class SettingsViewController: TableViewController { rewards: BraveRewards? = nil, windowProtection: WindowProtection?, braveCore: BraveCoreMain, - dau: DAU, + attributionManager: AttributionManager, keyringStore: KeyringStore? = nil, cryptoStore: CryptoStore? = nil ) { @@ -88,7 +88,7 @@ class SettingsViewController: TableViewController { self.syncAPI = braveCore.syncAPI self.syncProfileServices = braveCore.syncProfileService self.p3aUtilities = braveCore.p3aUtils - self.dau = dau + self.attributionManager = attributionManager self.keyringStore = keyringStore self.cryptoStore = cryptoStore self.ipfsAPI = braveCore.ipfsAPI @@ -864,7 +864,7 @@ class SettingsViewController: TableViewController { Row( text: "Retention Preferences Debug Menu", selection: { [unowned self] in - self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities, dau: dau), animated: true) + self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities, attributionManager: attributionManager), animated: true) }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row( text: "Load all QA Links", diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift b/Sources/Growth/URP/AttributionManager.swift similarity index 59% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift rename to Sources/Growth/URP/AttributionManager.swift index ea848064865..5696b569646 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Attribution.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -5,19 +5,29 @@ import Foundation import Preferences -import Growth import Shared -extension BrowserViewController { - public func handleReferralLookup(_ urp: UserReferralProgram) { +public class AttributionManager { + private let dau: DAU + private let urp: UserReferralProgram + + public init(dau: DAU, urp: UserReferralProgram) { + self.dau = dau + self.urp = urp + } + + public func handleReferralLookup(completion: @escaping (URL) -> Void) { if Preferences.URP.referralLookupOutstanding.value == true { - performProgramReferralLookup(urp, refCode: UserReferralProgram.getReferralCode()) + performProgramReferralLookup(refCode: UserReferralProgram.getReferralCode()) { offerUrl in + guard let url = offerUrl else { return } + completion(url) + } } else { urp.pingIfEnoughTimePassed() } } - @MainActor public func handleSearchAdsInstallAttribution(_ urp: UserReferralProgram) async throws { + @MainActor public func handleSearchAdsInstallAttribution() async throws { do { let attributionData = try await urp.adCampaignLookup() let refCode = urp.generateReferralCode(attributionData: attributionData) @@ -36,14 +46,11 @@ extension BrowserViewController { dau.sendPingToServer() } - private func performProgramReferralLookup(_ urp: UserReferralProgram, refCode: String?) { - urp.referralLookup(refCode: refCode) { [weak self] referralCode, offerUrl in - guard let self = self else { return } - + private func performProgramReferralLookup(refCode: String?, completion: @escaping (URL?) -> Void) { + urp.referralLookup(refCode: refCode) { referralCode, offerUrl in Preferences.URP.referralLookupOutstanding.value = false - guard let url = offerUrl?.asURL else { return } - self.openReferralLink(url: url) + completion(offerUrl?.asURL) } } } diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index 1d15af655e2..d90d0261ca3 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -29,7 +29,7 @@ struct UrpService { private let sessionManager: URLSession private let certificateEvaluator: URPCertificatePinningService - init?(host: String, apiKey: String, adServicesURL: String) { + init(host: String, apiKey: String, adServicesURL: String) { self.host = host self.apiKey = apiKey self.adServicesURL = adServicesURL diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index f887f1622d6..fd000c74bf1 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -37,20 +37,14 @@ public class UserReferralProgram { let service: UrpService - public init?() { + public init() { // This should _probably_ correspond to the baseUrl for NTPDownloader let host = AppConstants.buildChannel == .debug ? HostUrl.staging : HostUrl.prod - guard - let apiKey = Bundle.main.getPlistString( - for: UserReferralProgram.apiKeyPlistKey)? - .trimmingCharacters(in: .whitespacesAndNewlines) - else { - Logger.module.error("Urp init error, failed to get values from Brave.plist.") - return nil - } + let apiKey = Bundle.main.getPlistString(for: UserReferralProgram.apiKeyPlistKey)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "apikey" - guard let urpService = UrpService(host: host, apiKey: apiKey, adServicesURL: adServicesURLString) else { return nil } + let urpService = UrpService(host: host, apiKey: apiKey, adServicesURL: adServicesURLString) UrpLog.log("URP init, host: \(host)") diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 0cd2761e491..31f53975727 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -29,18 +29,16 @@ private enum WelcomeViewID: Int { public class WelcomeViewController: UIViewController { private var state: WelcomeViewCalloutState? private let p3aUtilities: BraveP3AUtils // Privacy Analytics - private let dau: DAU // Daily Active User - private let urp: UserReferralProgram? // User Referral which in sync with dau + private let attributionManager: AttributionManager // Manager to handle daily active user and user referral - public convenience init(p3aUtilities: BraveP3AUtils, dau: DAU) { - self.init(state: .loading, p3aUtilities: p3aUtilities, dau: dau) + public convenience init(p3aUtilities: BraveP3AUtils, attributionManager: AttributionManager) { + self.init(state: .loading, p3aUtilities: p3aUtilities, attributionManager: attributionManager) } - public init(state: WelcomeViewCalloutState?, p3aUtilities: BraveP3AUtils, dau: DAU) { + public init(state: WelcomeViewCalloutState?, p3aUtilities: BraveP3AUtils, attributionManager: AttributionManager) { self.state = state self.p3aUtilities = p3aUtilities - self.dau = dau - self.urp = UserReferralProgram.shared + self.attributionManager = attributionManager super.init(nibName: nil, bundle: nil) self.transitioningDelegate = self @@ -321,14 +319,14 @@ public class WelcomeViewController: UIViewController { } private func animateToWelcomeState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: self.dau).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: self.attributionManager).then { $0.setLayoutState(state: WelcomeViewCalloutState.welcome(title: Strings.Onboarding.welcomeScreenTitle)) } present(nextController, animated: true) } private func animateToDefaultBrowserState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: self.dau) + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: self.attributionManager) let state = WelcomeViewCalloutState.defaultBrowser( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.defaultBrowserCalloutTitle, @@ -349,7 +347,7 @@ public class WelcomeViewController: UIViewController { } private func animateToDefaultSettingsState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: self.dau).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: self.attributionManager).then { $0.setLayoutState( state: WelcomeViewCalloutState.settings( title: Strings.Onboarding.navigateSettingsOnboardingScreenTitle, @@ -362,7 +360,7 @@ public class WelcomeViewController: UIViewController { } private func animateToP3aState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, dau: dau) + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: attributionManager) let state = WelcomeViewCalloutState.p3a( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.p3aCalloutTitle, From 1164d8dcff509b88bac10c4b4f1f1477e23b1b6c Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Tue, 19 Dec 2023 14:23:24 -0500 Subject: [PATCH 08/20] Generate Code is moved --- App/iOS/Delegates/AppDelegate.swift | 37 +++++++++----------- App/iOS/Delegates/SceneDelegate.swift | 6 ++-- Sources/Growth/DAU.swift | 3 -- Sources/Growth/URP/AttributionManager.swift | 18 +++++++++- Sources/Growth/URP/UserReferralProgram.swift | 13 ------- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index fd6ea49acd0..7035764683f 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -203,30 +203,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Preferences.General.keepYouTubeInBrave.value = true } - if UserReferralProgram.shared != nil { - if Preferences.URP.referralLookupOutstanding.value == nil { - // This preference has never been set, and this means it is a new or upgraded user. - // That distinction must be made to know if a network request for ref-code look up should be made. - - // Setting this to an explicit value so it will never get overwritten on subsequent launches. - // Upgrade users should not have ref code ping happening. - Preferences.URP.referralLookupOutstanding.value = isFirstLaunch - } + if Preferences.URP.referralLookupOutstanding.value == nil { + // This preference has never been set, and this means it is a new or upgraded user. + // That distinction must be made to know if a network request for ref-code look up should be made. - SceneDelegate.shouldHandleUrpLookup = true - - 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 - } else { - log.error("Failed to initialize user referral program") - UrpLog.log("Failed to initialize user referral program") + // Setting this to an explicit value so it will never get overwritten on subsequent launches. + // Upgrade users should not have ref code ping happening. + Preferences.URP.referralLookupOutstanding.value = isFirstLaunch + } + + SceneDelegate.shouldHandleUrpLookup = true + + 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 4d067b3bfb3..80a9aba80cb 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -113,11 +113,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { try await attributionManager.handleSearchAdsInstallAttribution() } catch { Logger.module.debug("Error fetching ads attribution default code is sent \(error)") - attributionManager.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) + attributionManager.setupReferralCodeAndPingServer( + refCode: attributionManager.organicInstallReferralCode) } } } else { - attributionManager.setupReferralCodeAndPingServer(refCode: DAU.organicInstallReferralCode) + attributionManager.setupReferralCodeAndPingServer( + refCode: attributionManager.organicInstallReferralCode) } } } diff --git a/Sources/Growth/DAU.swift b/Sources/Growth/DAU.swift index fbcfc243814..1b0d92c94dd 100644 --- a/Sources/Growth/DAU.swift +++ b/Sources/Growth/DAU.swift @@ -24,9 +24,6 @@ public class DAU { } /// Number of seconds that determins when a user is "active" private let pingRefreshDuration = 5.minutes - - /// The default DAU referral code - public static let organicInstallReferralCode = "BRV001" /// We always use gregorian calendar for DAU pings. This also adds more anonymity to the server call. fileprivate static var calendar: Calendar { diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 5696b569646..9b03a048eb6 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -11,6 +11,9 @@ public class AttributionManager { private let dau: DAU private let urp: UserReferralProgram + /// The default Install Referral Code + public let organicInstallReferralCode = "BRV001" + public init(dau: DAU, urp: UserReferralProgram) { self.dau = dau self.urp = urp @@ -30,7 +33,7 @@ public class AttributionManager { @MainActor public func handleSearchAdsInstallAttribution() async throws { do { let attributionData = try await urp.adCampaignLookup() - let refCode = urp.generateReferralCode(attributionData: attributionData) + let refCode = generateReferralCode(attributionData: attributionData) setupReferralCodeAndPingServer(refCode: refCode) } catch { throw error @@ -53,4 +56,17 @@ public class AttributionManager { completion(offerUrl?.asURL) } } + + private func generateReferralCode(attributionData: AdAttributionData?) -> String { + // Prefix code "001" with BRV for organic iOS installs + var referralCode = organicInstallReferralCode + + if 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 + } } diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index fd000c74bf1..7cbd9f016b7 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -131,19 +131,6 @@ public class UserReferralProgram { throw error } } - - public func generateReferralCode(attributionData: AdAttributionData?) -> String { - // Prefix code "001" with BRV for organic iOS installs - var referralCode = DAU.organicInstallReferralCode - - if 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 initRetryPingConnection(numberOfTimes: Int32) { if AppConstants.buildChannel.isPublic { From 42cac3fc06dd37f9f6e49fcb0e7e4d615a9f00bb Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Wed, 20 Dec 2023 12:07:55 -0500 Subject: [PATCH 09/20] Adding notification for onboarding to browser controller --- .../BrowserViewController/BrowserViewController.swift | 10 ++++++++++ Sources/Growth/URP/AttributionManager.swift | 7 +++++++ Sources/Onboarding/Welcome/WelcomeViewController.swift | 2 ++ 3 files changed, 19 insertions(+) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index ccbb7a87ebe..b90ee4620ce 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -137,6 +137,7 @@ public class BrowserViewController: UIViewController { private var privateModeCancellable: AnyCancellable? private var appReviewCancelable: AnyCancellable? + private var adFeatureLinkageCancelable: AnyCancellable? var onPendingRequestUpdatedCancellable: AnyCancellable? /// Voice Search @@ -951,6 +952,15 @@ public class BrowserViewController: UIViewController { } }) + adFeatureLinkageCancelable = attributionManager + .$adFeatureLinkage + .removeDuplicates() + .sink(receiveValue: { [weak self] featureLinkageType in + guard let self = self else { return } + + print("Feature is linked \(featureLinkageType)") + }) + Preferences.General.isUsingBottomBar.objectWillChange .receive(on: RunLoop.main) .sink { [weak self] _ in diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 9b03a048eb6..9f982f25873 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -5,15 +5,22 @@ import Foundation import Preferences +import Combine import Shared public class AttributionManager { + public enum FeatureLinkageType { + case undefined, vpn, playlist + } + private let dau: DAU private let urp: UserReferralProgram /// The default Install Referral Code public let organicInstallReferralCode = "BRV001" + @Published public var adFeatureLinkage: FeatureLinkageType = .undefined + public init(dau: DAU, urp: UserReferralProgram) { self.dau = dau self.urp = urp diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 31f53975727..45e4c77d2a4 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -387,6 +387,8 @@ public class WelcomeViewController: UIViewController { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { nextController?.calloutView.isLoading = false + + nextController?.attributionManager.adFeatureLinkage = .vpn self?.close() } } From 5bf83a47eab9c34928f388e4798c0c77529627b2 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Thu, 21 Dec 2023 11:53:02 -0500 Subject: [PATCH 10/20] Adding fields to attribution data, sending ad feature link --- App/iOS/Delegates/SceneDelegate.swift | 8 ++--- Sources/Growth/URP/AdAttributionData.swift | 18 ++++++++++-- Sources/Growth/URP/AttributionManager.swift | 17 +++++++++-- Sources/Growth/URP/UrpService.swift | 21 +++++++++----- Sources/Growth/URP/UserReferralProgram.swift | 2 +- .../Welcome/WelcomeViewController.swift | 29 +++++++++++++------ 6 files changed, 69 insertions(+), 26 deletions(-) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 80a9aba80cb..d88e900e171 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -113,13 +113,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { try await attributionManager.handleSearchAdsInstallAttribution() } catch { Logger.module.debug("Error fetching ads attribution default code is sent \(error)") - attributionManager.setupReferralCodeAndPingServer( - refCode: attributionManager.organicInstallReferralCode) + // Sending default organic install code for dau + attributionManager.setupReferralCodeAndPingServer() } } } else { - attributionManager.setupReferralCodeAndPingServer( - refCode: attributionManager.organicInstallReferralCode) + // Sending default organic install code for dau + attributionManager.setupReferralCodeAndPingServer() } } } diff --git a/Sources/Growth/URP/AdAttributionData.swift b/Sources/Growth/URP/AdAttributionData.swift index 8f47c018998..ed408fd367c 100644 --- a/Sources/Growth/URP/AdAttributionData.swift +++ b/Sources/Growth/URP/AdAttributionData.swift @@ -18,13 +18,25 @@ public struct AdAttributionData { 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) { + // The ad group if for the campaign which will be used for feature link + public let adGroupId: Int? + // The keyword id for the campaign which will be used for feature link + public let keywordId: Int? + + init(attribution: Bool, + organizationId: Int? = nil, + conversionType: String? = nil, + campaignId: Int, + countryOrRegion: String? = nil, + adGroupId: Int? = nil, + keywordId: Int? = nil ) { self.attribution = attribution self.organizationId = organizationId self.conversionType = conversionType self.campaignId = campaignId self.countryOrRegion = countryOrRegion + self.adGroupId = adGroupId + self.keywordId = keywordId } } @@ -66,5 +78,7 @@ extension AdAttributionData { self.conversionType = json["conversionType"] as? String self.campaignId = campaignId self.countryOrRegion = json["countryOrRegion"] as? String + self.adGroupId = json["adGroupId"] as? Int + self.keywordId = json["keywordId"] as? Int } } diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 9f982f25873..d4939d178e2 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -17,7 +17,7 @@ public class AttributionManager { private let urp: UserReferralProgram /// The default Install Referral Code - public let organicInstallReferralCode = "BRV001" + private let organicInstallReferralCode = "BRV001" @Published public var adFeatureLinkage: FeatureLinkageType = .undefined @@ -47,7 +47,20 @@ public class AttributionManager { } } - public func setupReferralCodeAndPingServer(refCode: String) { + @MainActor public func handleAdsReportingFeatureLinkage() async throws -> FeatureLinkageType { + do { + // TODO: Test call result + let attributionData = try await urp.adCampaignLookup(isRetryEnabled: false) + return .vpn + + } catch { + throw error + } + } + + public func setupReferralCodeAndPingServer(refCode: String? = nil) { + let refCode = refCode ?? organicInstallReferralCode + // Setting up referral code value // This value should be set before first DAU ping Preferences.URP.referralCode.value = refCode diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index d90d0261ca3..57cb7df081f 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -78,7 +78,7 @@ struct UrpService { } } - @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String) async throws -> (AdAttributionData?) { + @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String, isRetryEnabled: Bool = true) async throws -> (AdAttributionData?) { guard let endPoint = URL(string: adServicesURL) else { Logger.module.error("AdServicesURLString can not be resolved: \(adServicesURL)") throw URLError(.badURL) @@ -87,7 +87,7 @@ struct UrpService { let attributionDataToken = adAttributionToken.data(using: .utf8) do { - let (result, _) = try await sessionManager.adServicesAttributionApiRequest(endPoint: endPoint, rawData: attributionDataToken) + let (result, _) = try await sessionManager.adServicesAttributionApiRequest(endPoint: endPoint, rawData: attributionDataToken, isRetryEnabled: isRetryEnabled) UrpLog.log("Ad Attribution response: \(result)") if let resultData = result as? Data { @@ -141,13 +141,18 @@ extension URLSession { } // 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) { + func adServicesAttributionApiRequest(endPoint: URL, rawData: Data?, isRetryEnabled: Bool) async throws -> (Any, URLResponse) { + // Re-try logic will not be enabled while onboarding happening on first launch + if isRetryEnabled { + // 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 + } else { return try await self.request(endPoint, method: .post, rawData: rawData, encoding: .textPlain) - }.value + } } } diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index 7cbd9f016b7..7a884f970c0 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -114,7 +114,7 @@ public class UserReferralProgram { service.referralCodeLookup(refCode: refCode, completion: referralBlock) } - @MainActor public func adCampaignLookup() async throws -> AdAttributionData? { + @MainActor public func adCampaignLookup(isRetryEnabled: Bool = true) async throws -> AdAttributionData? { // Fetching ad attibution token do { let adAttributionToken = try AAAttribution.attributionToken() diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 45e4c77d2a4..4d69a25ced1 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -379,17 +379,28 @@ public class WelcomeViewController: UIViewController { }, primaryButtonAction: { [weak nextController, weak self] in - guard nextController?.calloutView.isLoading == false else { + // Check controller is not in loading state + guard let controller = nextController, !controller.calloutView.isLoading else { return } - - nextController?.calloutView.isLoading = true - - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { - nextController?.calloutView.isLoading = false - - nextController?.attributionManager.adFeatureLinkage = .vpn - self?.close() + // The loading state should start before calling API + controller.calloutView.isLoading = true + + Task { @MainActor in + do { + // Handle API calls and send linkage type + let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() + controller.attributionManager.adFeatureLinkage = featureType + + controller.calloutView.isLoading = false + self?.close() + } catch { + // Sending default organic install code for dau + controller.attributionManager.setupReferralCodeAndPingServer() + + controller.calloutView.isLoading = false + self?.close() + } } } ) From 36b2665aa511a9b4aff6d6ae91b8452ffe1ffca8 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Thu, 21 Dec 2023 18:26:41 -0500 Subject: [PATCH 11/20] Add Report API calls and link keyword data --- ...ta.swift => AdAttributionReportData.swift} | 45 ++++++++++++++++--- Sources/Growth/URP/AttributionManager.swift | 43 ++++++++++++++++-- Sources/Growth/URP/UrpService.swift | 38 ++++++++++++++-- Sources/Growth/URP/UserReferralProgram.swift | 22 ++++++++- .../Welcome/WelcomeViewController.swift | 4 +- 5 files changed, 133 insertions(+), 19 deletions(-) rename Sources/Growth/URP/{AdAttributionData.swift => AdAttributionReportData.swift} (77%) diff --git a/Sources/Growth/URP/AdAttributionData.swift b/Sources/Growth/URP/AdAttributionReportData.swift similarity index 77% rename from Sources/Growth/URP/AdAttributionData.swift rename to Sources/Growth/URP/AdAttributionReportData.swift index ed408fd367c..7759c88f351 100644 --- a/Sources/Growth/URP/AdAttributionData.swift +++ b/Sources/Growth/URP/AdAttributionReportData.swift @@ -4,6 +4,12 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. import os.log +import Foundation + +public enum SerializationError: Error { + case missing(String) + case invalid(String, Any) +} 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. @@ -38,14 +44,7 @@ public struct AdAttributionData { self.adGroupId = adGroupId self.keywordId = keywordId } -} -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", "") @@ -82,3 +81,35 @@ extension AdAttributionData { self.keywordId = json["keywordId"] as? Int } } + +public struct AdGroupReportData { + public struct KeywordReportResponse: Codable { + let row: [KeywordRow] + } + + public struct KeywordRow: Codable { + let metadata: KeywordMetadata + } + + public struct KeywordMetadata: Codable { + let keyword: String + let keywordId: Int + } + + public let productKeyword: String + + init(data: Data, keywordId: Int) throws { + do { + let decoder = JSONDecoder() + let keywordResponse = try decoder.decode(KeywordReportResponse.self, from: data) + + if let keywordRow = keywordResponse.row.first(where: { $0.metadata.keywordId == keywordId }) { + productKeyword = keywordRow.metadata.keyword + } else { + throw SerializationError.invalid("Keyword with ID \(keywordId) not found", "") + } + } catch { + throw SerializationError.invalid("Invalid json Dictionary", "") + } + } +} diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index d4939d178e2..49254837282 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -13,6 +13,10 @@ public class AttributionManager { case undefined, vpn, playlist } + public enum FeatureLinkageError: Error { + case executionTimeout + } + private let dau: DAU private let urp: UserReferralProgram @@ -47,12 +51,43 @@ public class AttributionManager { } } - @MainActor public func handleAdsReportingFeatureLinkage() async throws -> FeatureLinkageType { + @MainActor public func handleAdsReportingFeatureLinkage() async throws -> String { + // This function should run multiple tasks first adCampaignLookup + // and adReportsKeywordLookup depending on adCampaignLookup result. + // There is maximum threshold of 5 sec for all the tasks to be completed + // or an error will be thrown + // This is done in order not to delay onboading more than certain periods + + let start = DispatchTime.now() // Start time for time tracking + do { - // TODO: Test call result - let attributionData = try await urp.adCampaignLookup(isRetryEnabled: false) - return .vpn + let attributionData = try await urp.adCampaignLookup() + + let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000 + let remainingTime = 5.0 - elapsedTime + + guard remainingTime > 0 else { + throw FeatureLinkageError.executionTimeout + } + + let task2Timeout = DispatchTime.now() + .seconds(Int(remainingTime)) + let keywordResult = try await withCheckedThrowingContinuation { continuation in + Task.detached { + do { + let keyword = try await self.urp.adReportsKeywordLookup(attributionData: attributionData) + continuation.resume(returning: keyword) + } catch { + continuation.resume(throwing: error) + } + } + + DispatchQueue.global().asyncAfter(deadline: task2Timeout) { + continuation.resume(throwing: FeatureLinkageError.executionTimeout) + } + } + + return keywordResult } catch { throw error } diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index 57cb7df081f..ecc249fef56 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -25,14 +25,16 @@ struct UrpService { private let host: String private let adServicesURL: String + private let adReportsURL: String private let apiKey: String private let sessionManager: URLSession private let certificateEvaluator: URPCertificatePinningService - init(host: String, apiKey: String, adServicesURL: String) { + init(host: String, apiKey: String, adServicesURL: String, adReportsURL: String) { self.host = host self.apiKey = apiKey self.adServicesURL = adServicesURL + self.adReportsURL = adReportsURL // Certificate pinning certificateEvaluator = URPCertificatePinningService() @@ -78,7 +80,7 @@ struct UrpService { } } - @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String, isRetryEnabled: Bool = true) async throws -> (AdAttributionData?) { + @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String, isRetryEnabled: Bool = true) async throws -> AdAttributionData { guard let endPoint = URL(string: adServicesURL) else { Logger.module.error("AdServicesURLString can not be resolved: \(adServicesURL)") throw URLError(.badURL) @@ -96,11 +98,35 @@ struct UrpService { return adAttributionData } + + throw SerializationError.invalid("Invalid Data type from response", "") + } catch { + throw error + } + } + + @MainActor func adGroupReportsKeywordLookup(adGroupId: Int, campaignId: Int, keywordId: Int) async throws -> String { + let reportsURL = adReportsURL + "campaigns/\(campaignId)/adgroups/\(adGroupId)/keywords" + + guard let endPoint = URL(string: reportsURL) else { + Logger.module.error("AdServicesURLString can not be resolved: \(reportsURL)") + throw URLError(.badURL) + } + + do { + let (result, _) = try await sessionManager.adGroupsReportApiRequest(endPoint: endPoint) + UrpLog.log("Ad Groups Report response: \(result)") + + if let resultData = result as? Data { + let adGroupsReportData = try AdGroupReportData(data: resultData, keywordId: keywordId) + + return adGroupsReportData.productKeyword + } + + throw SerializationError.invalid("Invalid Data type from response", "") } catch { throw error } - - return (nil) } func checkIfAuthorizedForGrant(with downloadId: String, completion: @escaping (Bool?, UrpError?) -> Void) { @@ -154,6 +180,10 @@ extension URLSession { return try await self.request(endPoint, method: .post, rawData: rawData, encoding: .textPlain) } } + + func adGroupsReportApiRequest(endPoint: URL) async throws -> (Any, URLResponse) { + return try await self.request(endPoint, method: .post, encoding: .json) + } } class URPCertificatePinningService: NSObject, URLSessionDelegate { diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index 7a884f970c0..c4427c90fe0 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -23,6 +23,7 @@ public class UserReferralProgram { } let adServicesURLString = "https://api-adservices.apple.com/api/v1/" + let adReportsURLString = "https://api.searchads.apple.com/api/v4/reports/" // In case of network problems when looking for referrral code // we retry the call few times while the app is still alive. @@ -44,7 +45,7 @@ public class UserReferralProgram { let apiKey = Bundle.main.getPlistString(for: UserReferralProgram.apiKeyPlistKey)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "apikey" - let urpService = UrpService(host: host, apiKey: apiKey, adServicesURL: adServicesURLString) + let urpService = UrpService(host: host, apiKey: apiKey, adServicesURL: adServicesURLString, adReportsURL: adReportsURLString) UrpLog.log("URP init, host: \(host)") @@ -114,7 +115,7 @@ public class UserReferralProgram { service.referralCodeLookup(refCode: refCode, completion: referralBlock) } - @MainActor public func adCampaignLookup(isRetryEnabled: Bool = true) async throws -> AdAttributionData? { + @MainActor public func adCampaignLookup(isRetryEnabled: Bool = true) async throws -> AdAttributionData { // Fetching ad attibution token do { let adAttributionToken = try AAAttribution.attributionToken() @@ -132,6 +133,23 @@ public class UserReferralProgram { } } + @MainActor func adReportsKeywordLookup(attributionData: AdAttributionData) async throws -> String { + guard let adGroupId = attributionData.adGroupId, let keywordId = attributionData.keywordId else { + throw SerializationError.invalid("adGroupId or keywordId is nil", "") + } + + do { + return try await service.adGroupReportsKeywordLookup( + adGroupId: adGroupId, + campaignId: attributionData.campaignId, + keywordId: keywordId) + + } catch { + Logger.module.info("Could not retrieve ad groups reports using ad services") + throw error + } + } + private func initRetryPingConnection(numberOfTimes: Int32) { if AppConstants.buildChannel.isPublic { // Adding some time offset to be extra safe. diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 4d69a25ced1..cb34ad02b98 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -389,8 +389,8 @@ public class WelcomeViewController: UIViewController { Task { @MainActor in do { // Handle API calls and send linkage type - let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() - controller.attributionManager.adFeatureLinkage = featureType +// let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() +// controller.attributionManager.adFeatureLinkage = featureType! controller.calloutView.isLoading = false self?.close() From 26974cf6d245a28d0c6f311455413fc6bca3a3e5 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 22 Dec 2023 14:55:04 -0500 Subject: [PATCH 12/20] Adding linage and showing proper feature handle screen --- .../BrowserViewController/BVC+Menu.swift | 2 +- .../BrowserViewController.swift | 8 ++++ Sources/BraveStrings/BraveStrings.swift | 1 - Sources/Growth/URP/AttributionManager.swift | 43 ++++++++++++++----- .../Welcome/WelcomeViewController.swift | 4 +- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift index c5691c39e3b..0c889177456 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Menu.swift @@ -208,7 +208,7 @@ extension BrowserViewController { } } - private func presentPlaylistController() { + public func presentPlaylistController() { if PlaylistCarplayManager.shared.isPlaylistControllerPresented { let alert = UIAlertController(title: Strings.PlayList.playlistAlreadyShowingTitle, message: Strings.PlayList.playlistAlreadyShowingBody, diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index b90ee4620ce..0c21398d095 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -957,6 +957,14 @@ public class BrowserViewController: UIViewController { .removeDuplicates() .sink(receiveValue: { [weak self] featureLinkageType in guard let self = self else { return } + switch featureLinkageType { + case .playlist: + self.presentPlaylistController() + case .vpn: + self.navigationHelper.openVPNBuyScreen(iapObserver: self.iapObserver) + default: + return + } print("Feature is linked \(featureLinkageType)") }) diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index a09c76e62f2..88cd7f3b13c 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -1237,7 +1237,6 @@ extension Strings { public static let requestCaptureDevicePermissionAllowButtonTitle = NSLocalizedString("requestCaptureDevicePermissionAllowButtonTitle", tableName: "BraveShared", bundle: .module, value: "Allow", comment: "A button shown in a permission dialog that will grant the website the ability to use the device's camera or microphone") public static let downloadsMenuItem = NSLocalizedString("DownloadsMenuItem", tableName: "BraveShared", bundle: .module, value: "Downloads", comment: "Title for downloads menu item") public static let downloadsPanelEmptyStateTitle = NSLocalizedString("DownloadsPanelEmptyStateTitle", tableName: "BraveShared", bundle: .module, value: "Downloaded files will show up here.", comment: "Title for when a user has nothing downloaded onto their device, and the list is empty.") - public static let playlistMenuItem = NSLocalizedString("PlaylistMenuItem", tableName: "BraveShared", bundle: .module, value: "Playlist", comment: "Playlist menu item") // MARK: - Themes diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 49254837282..e1309b6d7b5 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -8,14 +8,26 @@ import Preferences import Combine import Shared -public class AttributionManager { - public enum FeatureLinkageType { - case undefined, vpn, playlist - } +public enum FeatureLinkageType: CaseIterable { + case notdefined, vpn, playlist, leoAI - public enum FeatureLinkageError: Error { - case executionTimeout + var adKeywords: [String] { + switch self { + case .vpn: + return ["vpn, 1.1.1.1"] + case .playlist: + return ["youtube", "video player", "playlist"] + default: + return [] // Return nil for any other case + } } +} + +public enum FeatureLinkageError: Error { + case executionTimeout(AdAttributionData) +} + +public class AttributionManager { private let dau: DAU private let urp: UserReferralProgram @@ -23,7 +35,7 @@ public class AttributionManager { /// The default Install Referral Code private let organicInstallReferralCode = "BRV001" - @Published public var adFeatureLinkage: FeatureLinkageType = .undefined + @Published public var adFeatureLinkage: FeatureLinkageType = .notdefined public init(dau: DAU, urp: UserReferralProgram) { self.dau = dau @@ -51,7 +63,7 @@ public class AttributionManager { } } - @MainActor public func handleAdsReportingFeatureLinkage() async throws -> String { + @MainActor public func handleAdsReportingFeatureLinkage() async throws -> (featureType: FeatureLinkageType, attributionData: AdAttributionData) { // This function should run multiple tasks first adCampaignLookup // and adReportsKeywordLookup depending on adCampaignLookup result. // There is maximum threshold of 5 sec for all the tasks to be completed @@ -72,11 +84,13 @@ public class AttributionManager { let task2Timeout = DispatchTime.now() + .seconds(Int(remainingTime)) - let keywordResult = try await withCheckedThrowingContinuation { continuation in + let featureTypeResult = try await withCheckedThrowingContinuation { continuation in Task.detached { do { + self.generateReferralCodeAndPingServer(with: attributionData) let keyword = try await self.urp.adReportsKeywordLookup(attributionData: attributionData) - continuation.resume(returning: keyword) + let featureLinkageType = self.fetchFeatureTypes(for: keyword) + continuation.resume(returning: featureLinkageType) } catch { continuation.resume(throwing: error) } @@ -87,12 +101,19 @@ public class AttributionManager { } } - return keywordResult + return (featureTypeResult, attributionData) } catch { throw error } } + private func fetchFeatureTypes(for keyword: String) -> FeatureLinkageType { + for linkageType in FeatureLinkageType.allCases where linkageType.adKeywords.contains(keyword) { + return linkageType + } + return .notdefined + } + public func setupReferralCodeAndPingServer(refCode: String? = nil) { let refCode = refCode ?? organicInstallReferralCode diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index cb34ad02b98..9d3f8332244 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -389,8 +389,8 @@ public class WelcomeViewController: UIViewController { Task { @MainActor in do { // Handle API calls and send linkage type -// let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() -// controller.attributionManager.adFeatureLinkage = featureType! + let reportLink = try await controller.attributionManager.handleAdsReportingFeatureLinkage() + controller.attributionManager.adFeatureLinkage = reportLink.featureType controller.calloutView.isLoading = false self?.close() From f5568adf86744180fe1c5588ff26ddce18af9f46 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Tue, 2 Jan 2024 18:03:30 -0500 Subject: [PATCH 13/20] Handling all problem with early API call errors and responses --- App/iOS/Delegates/AppDelegate.swift | 7 --- App/iOS/Delegates/SceneDelegate.swift | 36 +++++++++------- .../Extensions/URLSessionExtensions.swift | 10 +++-- Sources/Growth/URP/AttributionManager.swift | 43 +++++++------------ Sources/Growth/URP/UrpService.swift | 3 +- .../Welcome/WelcomeViewController.swift | 11 +++-- 6 files changed, 54 insertions(+), 56 deletions(-) diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index 7035764683f..b86bed64ed2 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -213,13 +213,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } SceneDelegate.shouldHandleUrpLookup = true - - 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) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index d88e900e171..237f17d7765 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -97,6 +97,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { browserViewController?.openReferralLink(url: url) } } + + // Setup Playlist Car-Play + // TODO: Decide what to do if we have multiple windows + // as it is only possible to have a single car-play instance. + // Once we move to iOS 14+, this is easy to fix as we just pass car-play a `MediaStreamer` + // instance instead of a `BrowserViewController`. + PlaylistCarplayManager.shared.do { + $0.browserController = browserViewController + } + + self.present( + browserViewController: browserViewController, + windowScene: windowScene, + connectionOptions: connectionOptions + ) // Handle Install Attribution Fetch at first launch if SceneDelegate.shouldHandleInstallAttributionFetch { @@ -104,7 +119,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // First time user should send dau ping after onboarding last stage _ p3a consent screen // The reason p3a user consent is necesserray to call search ad install attribution API methods - if Preferences.AppState.dailyUserPingAwaitingUserConsent.value { + if !Preferences.AppState.dailyUserPingAwaitingUserConsent.value { // If P3A is not enabled, send the organic install code at daily pings which is BRV001 // User has not opted in to share completely private and anonymous product insights if AppState.shared.braveCore.p3aUtils.isP3AEnabled { @@ -123,21 +138,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } } - - // Setup Playlist Car-Play - // TODO: Decide what to do if we have multiple windows - // as it is only possible to have a single car-play instance. - // Once we move to iOS 14+, this is easy to fix as we just pass car-play a `MediaStreamer` - // instance instead of a `BrowserViewController`. - PlaylistCarplayManager.shared.do { - $0.browserController = browserViewController - } - self.present( - browserViewController: browserViewController, - windowScene: windowScene, - connectionOptions: connectionOptions - ) + 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 = Preferences.General.isFirstLaunch.value + } PrivacyReportsManager.scheduleNotification(debugMode: !AppConstants.buildChannel.isPublic) PrivacyReportsManager.consolidateData() diff --git a/Sources/BraveShared/Extensions/URLSessionExtensions.swift b/Sources/BraveShared/Extensions/URLSessionExtensions.swift index 3a503a1b319..2d7b33f0631 100644 --- a/Sources/BraveShared/Extensions/URLSessionExtensions.swift +++ b/Sources/BraveShared/Extensions/URLSessionExtensions.swift @@ -86,7 +86,8 @@ extension URLSession { headers: [String: String] = [:], parameters: [String: Any] = [:], rawData: Data? = nil, - encoding: ParameterEncoding = .query + encoding: ParameterEncoding = .query, + timeout: TimeInterval = 60 ) async throws -> (Any, URLResponse) { do { let request = try buildRequest( @@ -95,7 +96,8 @@ extension URLSession { headers: headers, parameters: parameters, rawData: rawData, - encoding: encoding) + encoding: encoding, + timeoutInterval: timeout) return try await data(for: request) } catch { @@ -126,11 +128,13 @@ extension URLSession { headers: [String: String], parameters: [String: Any], rawData: Data?, - encoding: ParameterEncoding + encoding: ParameterEncoding, + timeoutInterval: TimeInterval = 60 ) throws -> URLRequest { var request = URLRequest(url: url) request.httpMethod = method.rawValue + request.timeoutInterval = timeoutInterval headers.forEach({ request.setValue($0.value, forHTTPHeaderField: $0.key) }) switch encoding { case .textPlain: diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index e1309b6d7b5..b5a1b49e624 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -66,42 +66,31 @@ public class AttributionManager { @MainActor public func handleAdsReportingFeatureLinkage() async throws -> (featureType: FeatureLinkageType, attributionData: AdAttributionData) { // This function should run multiple tasks first adCampaignLookup // and adReportsKeywordLookup depending on adCampaignLookup result. - // There is maximum threshold of 5 sec for all the tasks to be completed - // or an error will be thrown - // This is done in order not to delay onboading more than certain periods - - let start = DispatchTime.now() // Start time for time tracking - + // There is a 60 sec timeout added for adCampaignLookup and will be run with no retry and + // additionally adGroupReportsKeywordLookup API call will be cancelled after 30 sec + do { - let attributionData = try await urp.adCampaignLookup() + let start = DispatchTime.now() // Start time for time tracking + + // Ad campaign Lookup should be performed with no retry as first step + let attributionData = try await urp.adCampaignLookup(isRetryEnabled: false) let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000 - let remainingTime = 5.0 - elapsedTime + let remainingTime = 1.0 - elapsedTime guard remainingTime > 0 else { throw FeatureLinkageError.executionTimeout } - - let task2Timeout = DispatchTime.now() + .seconds(Int(remainingTime)) - let featureTypeResult = try await withCheckedThrowingContinuation { continuation in - Task.detached { - do { - self.generateReferralCodeAndPingServer(with: attributionData) - let keyword = try await self.urp.adReportsKeywordLookup(attributionData: attributionData) - let featureLinkageType = self.fetchFeatureTypes(for: keyword) - continuation.resume(returning: featureLinkageType) - } catch { - continuation.resume(throwing: error) - } - } - - DispatchQueue.global().asyncAfter(deadline: task2Timeout) { - continuation.resume(throwing: FeatureLinkageError.executionTimeout) - } + do { + generateReferralCodeAndPingServer(with: attributionData) + + let keyword = try await urp.adReportsKeywordLookup(attributionData: attributionData) + let featureLinkageType = fetchFeatureTypes(for: keyword) + return (featureLinkageType, attributionData) + } catch { + throw(SearchAdError.successfulCampaignFailedKeywordLookup(attributionData)) } - - return (featureTypeResult, attributionData) } catch { throw error } diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index ecc249fef56..20a55d63b9d 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -182,7 +182,8 @@ extension URLSession { } func adGroupsReportApiRequest(endPoint: URL) async throws -> (Any, URLResponse) { - return try await self.request(endPoint, method: .post, encoding: .json) + // Having Reports Keywrod Lookup Endpoint 30 sec timeout + return try await self.request(endPoint, method: .post, encoding: .json, timeout: 30) } } diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 9d3f8332244..2dd75601fee 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -388,9 +388,14 @@ public class WelcomeViewController: UIViewController { Task { @MainActor in do { - // Handle API calls and send linkage type - let reportLink = try await controller.attributionManager.handleAdsReportingFeatureLinkage() - controller.attributionManager.adFeatureLinkage = reportLink.featureType + if controller.p3aUtilities.isP3AEnabled { + // Handle API calls and send linkage type + let reportLink = try await controller.attributionManager.handleAdsReportingFeatureLinkage() + controller.attributionManager.adFeatureLinkage = reportLink.featureType + } else { + // p3a consent is not given + controller.attributionManager.setupReferralCodeAndPingServer() + } controller.calloutView.isLoading = false self?.close() From 09c092d03ba4380243180c957c37a37994f251c4 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Wed, 3 Jan 2024 12:46:23 -0500 Subject: [PATCH 14/20] Ad Install Attribution is changed to campaignId --- Sources/Growth/URP/AttributionManager.swift | 49 ++++++++-- .../Welcome/WelcomeViewController.swift | 96 ++++++++++++++----- 2 files changed, 112 insertions(+), 33 deletions(-) diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index b5a1b49e624..144d0b291c1 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -18,7 +18,18 @@ public enum FeatureLinkageType: CaseIterable { case .playlist: return ["youtube", "video player", "playlist"] default: - return [] // Return nil for any other case + return [] + } + } + + var campaignIds: [Int] { + switch self { + case .vpn: + return [1475635127] + case .playlist: + return [1475635128] + default: + return [] } } } @@ -27,6 +38,10 @@ public enum FeatureLinkageError: Error { case executionTimeout(AdAttributionData) } +public enum FeatureLinkageLogicType { + case reporting, campaingId +} + public class AttributionManager { private let dau: DAU @@ -35,6 +50,8 @@ public class AttributionManager { /// The default Install Referral Code private let organicInstallReferralCode = "BRV001" + public let activeFetureLinkageLogic: FeatureLinkageLogicType = .campaingId + @Published public var adFeatureLinkage: FeatureLinkageType = .notdefined public init(dau: DAU, urp: UserReferralProgram) { @@ -53,17 +70,29 @@ public class AttributionManager { } } - @MainActor public func handleSearchAdsInstallAttribution() async throws { + @discardableResult + @MainActor public func handleSearchAdsInstallAttribution() async throws -> AdAttributionData { do { let attributionData = try await urp.adCampaignLookup() - let refCode = generateReferralCode(attributionData: attributionData) - setupReferralCodeAndPingServer(refCode: refCode) + generateReferralCodeAndPingServer(with: attributionData) + + return attributionData } catch { throw error } } - @MainActor public func handleAdsReportingFeatureLinkage() async throws -> (featureType: FeatureLinkageType, attributionData: AdAttributionData) { + @MainActor public func handleSearchAdsFeatureLinkage() async throws -> FeatureLinkageType { + do { + let attributionData = try await handleSearchAdsInstallAttribution() + + return fetchFeatureTypes(for: attributionData.campaignId) + } catch { + throw error + } + } + + @MainActor public func handleAdsReportingFeatureLinkage() async throws -> FeatureLinkageType { // This function should run multiple tasks first adCampaignLookup // and adReportsKeywordLookup depending on adCampaignLookup result. // There is a 60 sec timeout added for adCampaignLookup and will be run with no retry and @@ -86,8 +115,7 @@ public class AttributionManager { generateReferralCodeAndPingServer(with: attributionData) let keyword = try await urp.adReportsKeywordLookup(attributionData: attributionData) - let featureLinkageType = fetchFeatureTypes(for: keyword) - return (featureLinkageType, attributionData) + return fetchFeatureTypes(for: keyword) } catch { throw(SearchAdError.successfulCampaignFailedKeywordLookup(attributionData)) } @@ -103,6 +131,13 @@ public class AttributionManager { return .notdefined } + private func fetchFeatureTypes(for campaignId: Int) -> FeatureLinkageType { + for linkageType in FeatureLinkageType.allCases where linkageType.campaignIds.contains(campaignId) { + return linkageType + } + return .notdefined + } + public func setupReferralCodeAndPingServer(refCode: String? = nil) { let refCode = refCode ?? organicInstallReferralCode diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 2dd75601fee..946b2cf3b68 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -379,34 +379,11 @@ public class WelcomeViewController: UIViewController { }, primaryButtonAction: { [weak nextController, weak self] in - // Check controller is not in loading state - guard let controller = nextController, !controller.calloutView.isLoading else { + guard let controller = nextController, let self = self else { return } - // The loading state should start before calling API - controller.calloutView.isLoading = true - - Task { @MainActor in - do { - if controller.p3aUtilities.isP3AEnabled { - // Handle API calls and send linkage type - let reportLink = try await controller.attributionManager.handleAdsReportingFeatureLinkage() - controller.attributionManager.adFeatureLinkage = reportLink.featureType - } else { - // p3a consent is not given - controller.attributionManager.setupReferralCodeAndPingServer() - } - - controller.calloutView.isLoading = false - self?.close() - } catch { - // Sending default organic install code for dau - controller.attributionManager.setupReferralCodeAndPingServer() - - controller.calloutView.isLoading = false - self?.close() - } - } + + self.handleAdReportingFeatureLinkage(with: controller) } ) ) @@ -419,6 +396,73 @@ public class WelcomeViewController: UIViewController { } } + + private func handleAdReportingFeatureLinkage(with controller: WelcomeViewController) { + // Check controller is not in loading state + guard !controller.calloutView.isLoading else { + return + } + // The loading state should start before calling API + controller.calloutView.isLoading = true + + let attributionManager = controller.attributionManager + + Task { @MainActor in + do { + if controller.p3aUtilities.isP3AEnabled { + switch attributionManager.activeFetureLinkageLogic { + case .campaingId: + let featureType = try await attributionManager.handleSearchAdsFeatureLinkage() + attributionManager.adFeatureLinkage = featureType + case .reporting: + // Handle API calls and send linkage type + let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() + attributionManager.adFeatureLinkage = featureType + } + } else { + // p3a consent is not given + attributionManager.setupReferralCodeAndPingServer() + } + + controller.calloutView.isLoading = false + close() + } catch FeatureLinkageError.executionTimeout(let attributionData) { + // Time out occurred while executing ad reports lookup + // Ad Campaign Lookup is successful so dau server should be pinged + // attribution data referral code + await pingServerWithGeneratedReferralCode( + using: attributionData, controller: controller) + } catch SearchAdError.successfulCampaignFailedKeywordLookup(let attributionData) { + // Error occurred while executing ad reports lookup + // Ad Campaign Lookup is successful so dau server should be pinged + // attribution data referral code + await pingServerWithGeneratedReferralCode( + using: attributionData, controller: controller) + } catch { + // Error occurred before getting successful + // attributuion data, generic code should be pinged + attributionManager.setupReferralCodeAndPingServer() + + controller.calloutView.isLoading = false + close() + } + } + } + + private func pingServerWithGeneratedReferralCode(using attributionData: AdAttributionData, controller: WelcomeViewController) async { + Task { + await withCheckedContinuation { continuation in + DispatchQueue.global().async { + controller.attributionManager.generateReferralCodeAndPingServer(with: attributionData) + continuation.resume() + } + } + } + + controller.calloutView.isLoading = false + close() + } + private func onSetDefaultBrowser() { guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return From 5f14908233b6636006aed6400d0f0e3ae104606d Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Wed, 3 Jan 2024 13:20:59 -0500 Subject: [PATCH 15/20] Adding timeout to handle search ads --- Sources/Growth/URP/AttributionManager.swift | 5 +++-- Sources/Growth/URP/UrpService.swift | 12 ++++++++---- Sources/Growth/URP/UserReferralProgram.swift | 8 +++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 144d0b291c1..1d83cb0cc2d 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -84,8 +84,9 @@ public class AttributionManager { @MainActor public func handleSearchAdsFeatureLinkage() async throws -> FeatureLinkageType { do { - let attributionData = try await handleSearchAdsInstallAttribution() - + let attributionData = try await urp.adCampaignLookup(isRetryEnabled: false, timeout: 30) + generateReferralCodeAndPingServer(with: attributionData) + return fetchFeatureTypes(for: attributionData.campaignId) } catch { throw error diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index 20a55d63b9d..fb519d61248 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -80,7 +80,7 @@ struct UrpService { } } - @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String, isRetryEnabled: Bool = true) async throws -> AdAttributionData { + @MainActor func adCampaignTokenLookupQueue(adAttributionToken: String, isRetryEnabled: Bool = true, timeout: TimeInterval) async throws -> AdAttributionData { guard let endPoint = URL(string: adServicesURL) else { Logger.module.error("AdServicesURLString can not be resolved: \(adServicesURL)") throw URLError(.badURL) @@ -89,7 +89,11 @@ struct UrpService { let attributionDataToken = adAttributionToken.data(using: .utf8) do { - let (result, _) = try await sessionManager.adServicesAttributionApiRequest(endPoint: endPoint, rawData: attributionDataToken, isRetryEnabled: isRetryEnabled) + let (result, _) = try await sessionManager.adServicesAttributionApiRequest( + endPoint: endPoint, + rawData: attributionDataToken, + isRetryEnabled: isRetryEnabled, + timeout: timeout) UrpLog.log("Ad Attribution response: \(result)") if let resultData = result as? Data { @@ -167,7 +171,7 @@ extension URLSession { } // Apple ad service attricution request requires plain text encoding with post method and passing token as rawdata - func adServicesAttributionApiRequest(endPoint: URL, rawData: Data?, isRetryEnabled: Bool) async throws -> (Any, URLResponse) { + func adServicesAttributionApiRequest(endPoint: URL, rawData: Data?, isRetryEnabled: Bool, timeout: TimeInterval) async throws -> (Any, URLResponse) { // Re-try logic will not be enabled while onboarding happening on first launch if isRetryEnabled { // According to attributiontoken API docs @@ -177,7 +181,7 @@ extension URLSession { return try await self.request(endPoint, method: .post, rawData: rawData, encoding: .textPlain) }.value } else { - return try await self.request(endPoint, method: .post, rawData: rawData, encoding: .textPlain) + return try await self.request(endPoint, method: .post, rawData: rawData, encoding: .textPlain, timeout: timeout) } } diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index c4427c90fe0..48cc74dd8e8 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -115,14 +115,16 @@ public class UserReferralProgram { service.referralCodeLookup(refCode: refCode, completion: referralBlock) } - @MainActor public func adCampaignLookup(isRetryEnabled: Bool = true) async throws -> AdAttributionData { + @MainActor public func adCampaignLookup(isRetryEnabled: Bool = true, timeout: TimeInterval = 60) async throws -> AdAttributionData { // Fetching ad attibution token do { let adAttributionToken = try AAAttribution.attributionToken() do { - return try await service.adCampaignTokenLookupQueue(adAttributionToken: adAttributionToken) - + return try await service.adCampaignTokenLookupQueue( + adAttributionToken: adAttributionToken, + isRetryEnabled: isRetryEnabled, + timeout: timeout) } catch { Logger.module.info("Could not retrieve ad campaign attibution from ad services") throw error From 526e31c79643df76c861f2e5a635d04f3c1adea6 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Wed, 3 Jan 2024 17:18:19 -0500 Subject: [PATCH 16/20] Campaign Identifier reals added --- Sources/Growth/URP/AttributionManager.swift | 29 +++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 1d83cb0cc2d..863121e3a2f 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -25,9 +25,34 @@ public enum FeatureLinkageType: CaseIterable { var campaignIds: [Int] { switch self { case .vpn: - return [1475635127] + return [1475635127, + 1475635126, + 1485804102, + 1480261455, + 1480274213, + 1480244775, + 1485313498, + 1480289866, + 1480327301, + 1480338501, + 1485590008, + 1485843602, + 1480298771, + 1480271827, + 1485600647, + 1484999295, + 1480285450, + 1480274211] case .playlist: - return [1475635128] + return [1487969368, + 1489240861, + 1489108255, + 1488070608, + 1487607318, + 1487610419, + 1487610395, + 1488162143, + 1489145748] default: return [] } From 8fd1225f7e3b0b63ab96c1babae4b15c68f72d3c Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 5 Jan 2024 15:54:05 -0500 Subject: [PATCH 17/20] Fixing small problems and rebase problems --- .../Search/OpenSearchEngineButton.swift | 1 - .../Settings/SettingsViewController.swift | 4 +++- Sources/Growth/DAU.swift | 2 +- .../Growth/URP/AdAttributionReportData.swift | 24 +++++++++++++------ Sources/Growth/URP/AttributionManager.swift | 7 +++++- Sources/Growth/URP/UserReferralProgram.swift | 13 +++++----- .../Welcome/WelcomeViewController.swift | 9 ++++--- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift b/Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift index a737c8407e9..d68b8539a0f 100644 --- a/Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift +++ b/Sources/Brave/Frontend/Browser/Search/OpenSearchEngineButton.swift @@ -78,4 +78,3 @@ class OpenSearchEngineButton: BraveButton { } } } - diff --git a/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/Sources/Brave/Frontend/Settings/SettingsViewController.swift index 8f145b4ec29..17100493ffa 100644 --- a/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -864,7 +864,9 @@ class SettingsViewController: TableViewController { Row( text: "Retention Preferences Debug Menu", selection: { [unowned self] in - self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities, attributionManager: attributionManager), animated: true) + self.navigationController?.pushViewController( + RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities, attributionManager: attributionManager), + animated: true) }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row( text: "Load all QA Links", diff --git a/Sources/Growth/DAU.swift b/Sources/Growth/DAU.swift index 1b0d92c94dd..0a40e461c93 100644 --- a/Sources/Growth/DAU.swift +++ b/Sources/Growth/DAU.swift @@ -24,7 +24,7 @@ public class DAU { } /// Number of seconds that determins when a user is "active" private let pingRefreshDuration = 5.minutes - + /// We always use gregorian calendar for DAU pings. This also adds more anonymity to the server call. fileprivate static var calendar: Calendar { var cal = Calendar(identifier: .gregorian) diff --git a/Sources/Growth/URP/AdAttributionReportData.swift b/Sources/Growth/URP/AdAttributionReportData.swift index 7759c88f351..6eaacdac635 100644 --- a/Sources/Growth/URP/AdAttributionReportData.swift +++ b/Sources/Growth/URP/AdAttributionReportData.swift @@ -11,23 +11,33 @@ public enum SerializationError: Error { case invalid(String, Any) } +public enum SearchAdError: Error { + case invalidCampaignTokenData + case invalidGroupsReportData + case failedCampaignTokenFetch + case failedCampaignTokenLookup + case missingReportsKeywordParameter + case failedReportsKeywordLookup + case successfulCampaignFailedKeywordLookup(AdAttributionData) +} + 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 + 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? + let organizationId: Int? // The type of conversion is either Download or Redownload. - public let conversionType: String? + let conversionType: String? // The unique identifier for the campaign. - public let campaignId: Int + let campaignId: Int // The country or region for the campaign. - public let countryOrRegion: String? + let countryOrRegion: String? // The ad group if for the campaign which will be used for feature link - public let adGroupId: Int? + let adGroupId: Int? // The keyword id for the campaign which will be used for feature link - public let keywordId: Int? + let keywordId: Int? init(attribution: Bool, organizationId: Int? = nil, diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 863121e3a2f..a20849d6fcb 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -134,7 +134,7 @@ public class AttributionManager { let remainingTime = 1.0 - elapsedTime guard remainingTime > 0 else { - throw FeatureLinkageError.executionTimeout + throw FeatureLinkageError.executionTimeout(attributionData) } do { @@ -175,6 +175,11 @@ public class AttributionManager { dau.sendPingToServer() } + public func generateReferralCodeAndPingServer(with attributionData: AdAttributionData) { + let refCode = generateReferralCode(attributionData: attributionData) + setupReferralCodeAndPingServer(refCode: refCode) + } + private func performProgramReferralLookup(refCode: String?, completion: @escaping (URL?) -> Void) { urp.referralLookup(refCode: refCode) { referralCode, offerUrl in Preferences.URP.referralLookupOutstanding.value = false diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index 48cc74dd8e8..f43a64a5994 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -127,28 +127,29 @@ public class UserReferralProgram { timeout: timeout) } catch { Logger.module.info("Could not retrieve ad campaign attibution from ad services") - throw error + throw SearchAdError.invalidCampaignTokenData } } catch { Logger.module.info("Couldnt fetch attribute tokens with error: \(error)") - throw error + throw SearchAdError.failedCampaignTokenFetch } } @MainActor func adReportsKeywordLookup(attributionData: AdAttributionData) async throws -> String { guard let adGroupId = attributionData.adGroupId, let keywordId = attributionData.keywordId else { - throw SerializationError.invalid("adGroupId or keywordId is nil", "") + Logger.module.info("Could not retrieve ad campaign attibution from ad services") + throw SearchAdError.missingReportsKeywordParameter } - + do { return try await service.adGroupReportsKeywordLookup( adGroupId: adGroupId, campaignId: attributionData.campaignId, keywordId: keywordId) - + } catch { Logger.module.info("Could not retrieve ad groups reports using ad services") - throw error + throw SearchAdError.failedReportsKeywordLookup } } diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index 946b2cf3b68..8e651e10872 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -319,14 +319,14 @@ public class WelcomeViewController: UIViewController { } private func animateToWelcomeState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: self.attributionManager).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: p3aUtilities, attributionManager: attributionManager).then { $0.setLayoutState(state: WelcomeViewCalloutState.welcome(title: Strings.Onboarding.welcomeScreenTitle)) } present(nextController, animated: true) } private func animateToDefaultBrowserState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: self.attributionManager) + let nextController = WelcomeViewController(state: nil, p3aUtilities: p3aUtilities, attributionManager: attributionManager) let state = WelcomeViewCalloutState.defaultBrowser( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.defaultBrowserCalloutTitle, @@ -347,7 +347,7 @@ public class WelcomeViewController: UIViewController { } private func animateToDefaultSettingsState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: self.attributionManager).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: p3aUtilities, attributionManager: attributionManager).then { $0.setLayoutState( state: WelcomeViewCalloutState.settings( title: Strings.Onboarding.navigateSettingsOnboardingScreenTitle, @@ -360,7 +360,7 @@ public class WelcomeViewController: UIViewController { } private func animateToP3aState() { - let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities, attributionManager: attributionManager) + let nextController = WelcomeViewController(state: nil, p3aUtilities: p3aUtilities, attributionManager: attributionManager) let state = WelcomeViewCalloutState.p3a( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.p3aCalloutTitle, @@ -396,7 +396,6 @@ public class WelcomeViewController: UIViewController { } } - private func handleAdReportingFeatureLinkage(with controller: WelcomeViewController) { // Check controller is not in loading state guard !controller.calloutView.isLoading else { From 2f50527062face8d14fccd78921658b47b7e35c4 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 5 Jan 2024 15:58:13 -0500 Subject: [PATCH 18/20] Moving left BV classes folder structure --- .../BVC+Playlist.swift} | 0 ...ller+WKDownloadDelegate.swift => BVC+WKDownloadDelegate.swift} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Sources/Brave/Frontend/Browser/{Playlist/Browser/BrowserViewController+Playlist.swift => BrowserViewController/BVC+Playlist.swift} (100%) rename Sources/Brave/Frontend/Browser/BrowserViewController/{BrowserViewController+WKDownloadDelegate.swift => BVC+WKDownloadDelegate.swift} (100%) diff --git a/Sources/Brave/Frontend/Browser/Playlist/Browser/BrowserViewController+Playlist.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Playlist.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/Playlist/Browser/BrowserViewController+Playlist.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Playlist.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKDownloadDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKDownloadDelegate.swift similarity index 100% rename from Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKDownloadDelegate.swift rename to Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKDownloadDelegate.swift From 032aefbf64d46f2abee234386fdc176f3a35f704 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 5 Jan 2024 16:26:02 -0500 Subject: [PATCH 19/20] Pull Request Comments are addressed enum case and log problem --- .../BrowserViewController.swift | 2 -- Sources/Growth/URP/AttributionManager.swift | 16 ++++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index 0c21398d095..375638c6e9c 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -965,8 +965,6 @@ public class BrowserViewController: UIViewController { default: return } - - print("Feature is linked \(featureLinkageType)") }) Preferences.General.isUsingBottomBar.objectWillChange diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index a20849d6fcb..1d55cc51081 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -9,7 +9,7 @@ import Combine import Shared public enum FeatureLinkageType: CaseIterable { - case notdefined, vpn, playlist, leoAI + case vpn, playlist, leoAI var adKeywords: [String] { switch self { @@ -77,7 +77,7 @@ public class AttributionManager { public let activeFetureLinkageLogic: FeatureLinkageLogicType = .campaingId - @Published public var adFeatureLinkage: FeatureLinkageType = .notdefined + @Published public var adFeatureLinkage: FeatureLinkageType? public init(dau: DAU, urp: UserReferralProgram) { self.dau = dau @@ -107,7 +107,7 @@ public class AttributionManager { } } - @MainActor public func handleSearchAdsFeatureLinkage() async throws -> FeatureLinkageType { + @MainActor public func handleSearchAdsFeatureLinkage() async throws -> FeatureLinkageType? { do { let attributionData = try await urp.adCampaignLookup(isRetryEnabled: false, timeout: 30) generateReferralCodeAndPingServer(with: attributionData) @@ -118,7 +118,7 @@ public class AttributionManager { } } - @MainActor public func handleAdsReportingFeatureLinkage() async throws -> FeatureLinkageType { + @MainActor public func handleAdsReportingFeatureLinkage() async throws -> FeatureLinkageType? { // This function should run multiple tasks first adCampaignLookup // and adReportsKeywordLookup depending on adCampaignLookup result. // There is a 60 sec timeout added for adCampaignLookup and will be run with no retry and @@ -150,18 +150,18 @@ public class AttributionManager { } } - private func fetchFeatureTypes(for keyword: String) -> FeatureLinkageType { + private func fetchFeatureTypes(for keyword: String) -> FeatureLinkageType? { for linkageType in FeatureLinkageType.allCases where linkageType.adKeywords.contains(keyword) { return linkageType } - return .notdefined + return nil } - private func fetchFeatureTypes(for campaignId: Int) -> FeatureLinkageType { + private func fetchFeatureTypes(for campaignId: Int) -> FeatureLinkageType? { for linkageType in FeatureLinkageType.allCases where linkageType.campaignIds.contains(campaignId) { return linkageType } - return .notdefined + return nil } public func setupReferralCodeAndPingServer(refCode: String? = nil) { From b923721b1e38e626aa1c8c41430c83f3433580b8 Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 5 Jan 2024 17:00:45 -0500 Subject: [PATCH 20/20] Pull Request Comments are addressed append campaign keywords groups id --- Sources/Growth/URP/UrpService.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index fb519d61248..beccac3efb2 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -110,13 +110,16 @@ struct UrpService { } @MainActor func adGroupReportsKeywordLookup(adGroupId: Int, campaignId: Int, keywordId: Int) async throws -> String { - let reportsURL = adReportsURL + "campaigns/\(campaignId)/adgroups/\(adGroupId)/keywords" - - guard let endPoint = URL(string: reportsURL) else { - Logger.module.error("AdServicesURLString can not be resolved: \(reportsURL)") + guard let reportsURL = URL(string: adReportsURL) else { + Logger.module.error("AdServicesURLString can not be resolved: \(adReportsURL)") throw URLError(.badURL) } + var endPoint = reportsURL + endPoint.append(pathComponents: "campaigns", "\(campaignId)") + endPoint.append(pathComponents: "adgroups", "\(adGroupId)") + endPoint.append(pathComponents: "keywords", "") + do { let (result, _) = try await sessionManager.adGroupsReportApiRequest(endPoint: endPoint) UrpLog.log("Ad Groups Report response: \(result)")