diff --git a/Core/AppConfigurationURLProvider.swift b/Core/AppConfigurationURLProvider.swift index 7481d8eb93..262b066655 100644 --- a/Core/AppConfigurationURLProvider.swift +++ b/Core/AppConfigurationURLProvider.swift @@ -32,6 +32,7 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { case .trackerDataSet: return URL.trackerDataSet case .surrogates: return URL.surrogates case .FBConfig: fatalError("This feature is not supported on iOS") + case .remoteMessagingConfig: return RemoteMessagingClient.Constants.endpoint } } diff --git a/Core/Configuration.swift b/Core/Configuration.swift index e7c9dcb631..513389febf 100644 --- a/Core/Configuration.swift +++ b/Core/Configuration.swift @@ -31,6 +31,7 @@ public extension Configuration { case .surrogates: return "surrogates" case .trackerDataSet: return "trackerDataSet" case .FBConfig: return "FBConfig" + case .remoteMessagingConfig: return "remoteMessagingConfig" } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 57fc9a1480..01a7899b30 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -184,6 +184,7 @@ 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F962A155F7C0029F789 /* SyncDataProviders.swift */; }; 3760DFED299315EF0045A446 /* Waitlist in Frameworks */ = {isa = PBXBuildFile; productRef = 3760DFEC299315EF0045A446 /* Waitlist */; }; + 3768D8472C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3768D8462C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift */; }; 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; 37A6A8FE2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8FD2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift */; }; @@ -217,7 +218,6 @@ 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */; }; 4B6ED9452B992FE4007F5CAA /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */; }; 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */; }; - 4B78074E2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */; }; 4B948E2629DCCDB9002531FA /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 4B948E2529DCCDB9002531FA /* Persistence */; }; 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */; }; 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BBBBA862B02E85400D965DA /* DesignResourcesKit */; }; @@ -1305,6 +1305,7 @@ 3736088F2ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeStorage.swift; sourceTree = ""; }; 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoritesDisplayMode+UserDefaults.swift"; sourceTree = ""; }; 37445F962A155F7C0029F789 /* SyncDataProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataProviders.swift; sourceTree = ""; }; + 3768D8462C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingConfigMatcherProvider.swift; sourceTree = ""; }; 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeSyncHandler.swift; sourceTree = ""; }; 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCleanupErrorHandling.swift; sourceTree = ""; }; 37A6A8FD2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboarding.swift; sourceTree = ""; }; @@ -1335,7 +1336,6 @@ 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuControllerView.swift; sourceTree = ""; }; 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintingUserScript.swift; sourceTree = ""; }; - 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingSurveyURLBuilder.swift; sourceTree = ""; }; 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWidget.swift; sourceTree = ""; }; 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistUserText.swift; sourceTree = ""; }; 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; @@ -3355,7 +3355,6 @@ isa = PBXGroup; children = ( 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */, - 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */, BDFF031F2BA3D3AD00F324C9 /* Feature Visibility */, ); name = VPN; @@ -4421,6 +4420,7 @@ isa = PBXGroup; children = ( C1B7B52128941F2A0098FD6A /* RemoteMessagingClient.swift */, + 3768D8462C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift */, 3712091D2C21E390003ADF3D /* RemoteMessagingStoreErrorHandling.swift */, ); name = RemoteMessaging; @@ -6522,6 +6522,7 @@ B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, + 3768D8472C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */, 1D200C992BA3176D00108701 /* SettingsOthersView.swift in Sources */, 31DD208427395A5A008FB313 /* VoiceSearchHelper.swift in Sources */, 9874F9EE2187AFCE00CAF33D /* Themable.swift in Sources */, @@ -6576,7 +6577,6 @@ F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */, 37CF91602BB4737300BADCAE /* CrashCollectionOnboarding.swift in Sources */, C1B924B72ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift in Sources */, - 4B78074E2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift in Sources */, 3132FA2A27A0788F00DD7A12 /* QuickLookPreviewHelper.swift in Sources */, D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */, C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */, @@ -9946,7 +9946,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 167.0.1; + version = 169.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d2d55714a..804d062cbb 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "0746af01b77d39a1e037bea93b46591534a13b5c", - "version" : "167.0.1" + "revision" : "bfabf4518a33eb2b4b11003a15633a24c28fa922", + "version" : "169.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "7ac68ae3bc052fa59adbc1ba8fd5cb5849a6bc99", - "version" : "5.25.0" + "revision" : "9c65477457126ab7ad963a32b7f85ce08e6bd1a7", + "version" : "6.0.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 92d0396ef9..3cdd86b290 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -32,6 +32,7 @@ import Crashes import Configuration import Networking import DDGSync +import RemoteMessaging import SyncDataProviders import Subscription @@ -79,6 +80,10 @@ import WebKit private var showKeyboardIfSettingOn = true private var lastBackgroundDate: Date? + private(set) var homePageConfiguration: HomePageConfiguration! + + private(set) var remoteMessagingClient: RemoteMessagingClient! + private(set) var syncService: DDGSync! private(set) var syncDataProviders: SyncDataProviders! private var syncDidFinishCancellable: AnyCancellable? @@ -278,6 +283,22 @@ import WebKit }) } + remoteMessagingClient = RemoteMessagingClient( + bookmarksDatabase: bookmarksDatabase, + appSettings: AppDependencyProvider.shared.appSettings, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + configurationStore: ConfigurationStore.shared, + database: Database.shared, + errorEvents: RemoteMessagingStoreErrorHandling(), + remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager + ) + ) + remoteMessagingClient.registerBackgroundRefreshTaskHandler() + + homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient) + let previewsSource = TabPreviewsSource() let historyManager = makeHistoryManager(AppDependencyProvider.shared.appSettings, AppDependencyProvider.shared.internalUserDecider, @@ -287,6 +308,7 @@ import WebKit let main = MainViewController(bookmarksDatabase: bookmarksDatabase, bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, historyManager: historyManager, + homePageConfiguration: homePageConfiguration, syncService: syncService, syncDataProviders: syncDataProviders, appSettings: AppDependencyProvider.shared.appSettings, @@ -318,11 +340,6 @@ import WebKit // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. AppConfigurationFetch.registerBackgroundRefreshTaskHandler() - RemoteMessagingClient.registerBackgroundRefreshTaskHandler( - bookmarksDatabase: bookmarksDatabase, - favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode - ) - UNUserNotificationCenter.current().delegate = self window?.windowScene?.screenshotService?.delegate = self @@ -616,10 +633,7 @@ import WebKit private func refreshRemoteMessages() { Task { - try? await RemoteMessagingClient.fetchAndProcess( - bookmarksDatabase: self.bookmarksDatabase, - favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode - ) + try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) } } diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 6ccf795c6e..6cc6b5c117 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -33,8 +33,6 @@ protocol DependencyProvider { var variantManager: VariantManager { get } var internalUserDecider: InternalUserDecider { get } var featureFlagger: FeatureFlagger { get } - var remoteMessagingStore: RemoteMessagingStore { get } - var homePageConfiguration: HomePageConfiguration { get } var storageCache: StorageCache { get } var voiceSearchHelper: VoiceSearchHelperProtocol { get } var downloadManager: DownloadManager { get } @@ -64,13 +62,6 @@ class AppDependencyProvider: DependencyProvider { let internalUserDecider: InternalUserDecider = ContentBlocking.shared.privacyConfigurationManager.internalUserDecider let featureFlagger: FeatureFlagger - let remoteMessagingStore: RemoteMessagingStore = RemoteMessagingStore( - database: Database.shared, - errorEvents: RemoteMessagingStoreErrorHandling(), - log: .remoteMessaging - ) - lazy var homePageConfiguration: HomePageConfiguration = HomePageConfiguration(variantManager: variantManager, - remoteMessagingStore: remoteMessagingStore) let storageCache = StorageCache() let voiceSearchHelper: VoiceSearchHelperProtocol = VoiceSearchHelper() let downloadManager = DownloadManager() diff --git a/DuckDuckGo/ConfigurationDebugViewController.swift b/DuckDuckGo/ConfigurationDebugViewController.swift index 15d7c25392..1c8afce50a 100644 --- a/DuckDuckGo/ConfigurationDebugViewController.swift +++ b/DuckDuckGo/ConfigurationDebugViewController.swift @@ -54,6 +54,7 @@ class ConfigurationDebugViewController: UITableViewController { case surrogates case trackerDataSet case privacyConfiguration + case remoteMessagingConfig case resetEtags = "Reset ETags" var showDetail: Bool { diff --git a/DuckDuckGo/ConfigurationURLDebugViewController.swift b/DuckDuckGo/ConfigurationURLDebugViewController.swift index 8de0d0aa2f..bd217e3940 100644 --- a/DuckDuckGo/ConfigurationURLDebugViewController.swift +++ b/DuckDuckGo/ConfigurationURLDebugViewController.swift @@ -180,6 +180,7 @@ struct CustomConfigurationURLProvider: ConfigurationURLProviding { var customTrackerDataSetURL: URL? var customSurrogatesURL: URL? var customFBConfigURL: URL? + var customRemoteMessagingConfigURL: URL? let defaultProvider = AppConfigurationURLProvider() @@ -194,6 +195,7 @@ struct CustomConfigurationURLProvider: ConfigurationURLProviding { case .trackerDataSet: customURL = customTrackerDataSetURL case .surrogates: customURL = customSurrogatesURL case .FBConfig: customURL = nil + case .remoteMessagingConfig: customURL = customRemoteMessagingConfigURL } return customURL ?? defaultURL } diff --git a/DuckDuckGo/HomeCollectionView.swift b/DuckDuckGo/HomeCollectionView.swift index 729b30f92e..9d29f3e9e3 100644 --- a/DuckDuckGo/HomeCollectionView.swift +++ b/DuckDuckGo/HomeCollectionView.swift @@ -27,15 +27,14 @@ class HomeCollectionView: UICollectionView { static let topInset: CGFloat = 79 } + var homePageConfiguration: HomePageConfiguration! private weak var controller: HomeViewController! private(set) var renderers: HomeViewSectionRenderers! private lazy var collectionViewReorderingGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.collectionViewReorderingGestureHandler(gesture:))) - - private lazy var homePageConfiguration = AppDependencyProvider.shared.homePageConfiguration - + private var topIndexPath: IndexPath? { for section in 0.. 0 { return IndexPath(row: 0, section: section) diff --git a/DuckDuckGo/HomePageConfiguration.swift b/DuckDuckGo/HomePageConfiguration.swift index b7832ef2a3..14aa2c37d1 100644 --- a/DuckDuckGo/HomePageConfiguration.swift +++ b/DuckDuckGo/HomePageConfiguration.swift @@ -44,14 +44,13 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { // MARK: - Messages private var homeMessageStorage: HomeMessageStorage - private var remoteMessagingStore: RemoteMessagingStore + private var remoteMessagingClient: RemoteMessagingClient var homeMessages: [HomeMessage] = [] - init(variantManager: VariantManager? = nil, - remoteMessagingStore: RemoteMessagingStore = AppDependencyProvider.shared.remoteMessagingStore) { + init(variantManager: VariantManager? = nil, remoteMessagingClient: RemoteMessagingClient) { homeMessageStorage = HomeMessageStorage(variantManager: variantManager) - self.remoteMessagingStore = remoteMessagingStore + self.remoteMessagingClient = remoteMessagingClient homeMessages = buildHomeMessages() } @@ -75,7 +74,7 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { } private func remoteMessageToShow() -> HomeMessage? { - guard let remoteMessageToPresent = remoteMessagingStore.fetchScheduledRemoteMessage() else { return nil } + guard let remoteMessageToPresent = remoteMessagingClient.store.fetchScheduledRemoteMessage() else { return nil } os_log("Remote message to show: %s", log: .remoteMessaging, type: .info, remoteMessageToPresent.id) return .remoteMessage(remoteMessage: remoteMessageToPresent) } @@ -84,7 +83,7 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { switch homeMessage { case .remoteMessage(let remoteMessage): os_log("Home message dismissed: %s", log: .remoteMessaging, type: .info, remoteMessage.id) - remoteMessagingStore.dismissRemoteMessage(withId: remoteMessage.id) + remoteMessagingClient.store.dismissRemoteMessage(withID: remoteMessage.id) if let index = homeMessages.firstIndex(of: homeMessage) { homeMessages.remove(at: index) @@ -102,11 +101,11 @@ final class HomePageConfiguration: HomePageMessagesConfiguration { Pixel.fire(pixel: .remoteMessageShown, withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) - if !remoteMessagingStore.hasShownRemoteMessage(withId: remoteMessage.id) { + if !remoteMessagingClient.store.hasShownRemoteMessage(withID: remoteMessage.id) { os_log("Remote message shown for first time: %s", log: .remoteMessaging, type: .info, remoteMessage.id) Pixel.fire(pixel: .remoteMessageShownUnique, withAdditionalParameters: [PixelParameters.message: "\(remoteMessage.id)"]) - remoteMessagingStore.updateRemoteMessage(withId: remoteMessage.id, asShown: true) + remoteMessagingClient.store.updateRemoteMessage(withID: remoteMessage.id, asShown: true) } default: diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index ffe182474e..dad56cca12 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -68,6 +68,7 @@ class HomeViewController: UIViewController, NewTabPage { private var viewHasAppeared = false private var defaultVerticalAlignConstant: CGFloat = 0 + private let homePageConfiguration: HomePageConfiguration private let tabModel: Tab private let favoritesViewModel: FavoritesListInteracting private let appSettings: AppSettings @@ -76,7 +77,9 @@ class HomeViewController: UIViewController, NewTabPage { private var viewModelCancellable: AnyCancellable? private var favoritesDisplayModeCancellable: AnyCancellable? + // swiftlint:disable:next function_parameter_count static func loadFromStoryboard( + homePageConfiguration: HomePageConfiguration, model: Tab, favoritesViewModel: FavoritesListInteracting, appSettings: AppSettings, @@ -87,6 +90,7 @@ class HomeViewController: UIViewController, NewTabPage { let controller = storyboard.instantiateViewController(identifier: "HomeViewController", creator: { coder in HomeViewController( coder: coder, + homePageConfiguration: homePageConfiguration, tabModel: model, favoritesViewModel: favoritesViewModel, appSettings: appSettings, @@ -99,12 +103,14 @@ class HomeViewController: UIViewController, NewTabPage { required init?( coder: NSCoder, + homePageConfiguration: HomePageConfiguration, tabModel: Tab, favoritesViewModel: FavoritesListInteracting, appSettings: AppSettings, syncService: DDGSyncing, syncDataProviders: SyncDataProviders ) { + self.homePageConfiguration = homePageConfiguration self.tabModel = tabModel self.favoritesViewModel = favoritesViewModel self.appSettings = appSettings @@ -124,6 +130,7 @@ class HomeViewController: UIViewController, NewTabPage { NotificationCenter.default.addObserver(self, selector: #selector(HomeViewController.onKeyboardChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + collectionView.homePageConfiguration = homePageConfiguration configureCollectionView() decorate() @@ -160,9 +167,11 @@ class HomeViewController: UIViewController, NewTabPage { } @objc func remoteMessagesDidChange() { - os_log("Remote messages did change", log: .remoteMessaging, type: .info) - collectionView.refreshHomeConfiguration() - refresh() + DispatchQueue.main.async { + os_log("Remote messages did change", log: .remoteMessaging, type: .info) + self.collectionView.refreshHomeConfiguration() + self.refresh() + } } func configureCollectionView() { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 422e2eac4c..f125b722da 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -90,6 +90,7 @@ class MainViewController: UIViewController { var tabsBarController: TabsBarViewController? var suggestionTrayController: SuggestionTrayViewController? + let homePageConfiguration: HomePageConfiguration let homeTabManager: NewTabPageManager let tabManager: TabManager let previewsSource: TabPreviewsSource @@ -177,6 +178,7 @@ class MainViewController: UIViewController { bookmarksDatabase: CoreDataDatabase, bookmarksDatabaseCleaner: BookmarkDatabaseCleaner, historyManager: HistoryManager, + homePageConfiguration: HomePageConfiguration, syncService: DDGSyncing, syncDataProviders: SyncDataProviders, appSettings: AppSettings, @@ -187,6 +189,7 @@ class MainViewController: UIViewController { self.bookmarksDatabase = bookmarksDatabase self.bookmarksDatabaseCleaner = bookmarksDatabaseCleaner self.historyManager = historyManager + self.homePageConfiguration = homePageConfiguration self.syncService = syncService self.syncDataProviders = syncDataProviders self.favoritesViewModel = FavoritesListViewModel(bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: appSettings.favoritesDisplayMode) @@ -678,7 +681,7 @@ class MainViewController: UIViewController { private func addLaunchTabNotificationObserver() { launchTabObserver = LaunchTabNotification.addObserver(handler: { [weak self] urlString in guard let self = self else { return } - if let url = URL(string: urlString) { + if let url = URL(trimmedAddressBarString: urlString), url.isValid { self.loadUrlInNewTab(url, inheritedAttribution: nil) } else { self.loadQuery(urlString) @@ -728,7 +731,7 @@ class MainViewController: UIViewController { currentTab?.dismiss() removeHomeScreen() - AppDependencyProvider.shared.homePageConfiguration.refresh() + homePageConfiguration.refresh() // Access the tab model directly as we don't want to create a new tab controller here guard let tabModel = tabManager.model.currentTab else { @@ -736,12 +739,13 @@ class MainViewController: UIViewController { } if homeTabManager.isNewTabPageSectionsEnabled { - let controller = NewTabPageViewController() + let controller = NewTabPageViewController(homePageMessagesConfiguration: homePageConfiguration) newTabPageViewController = controller addToContentContainer(controller: controller) viewCoordinator.logoContainer.isHidden = true } else { - let controller = HomeViewController.loadFromStoryboard(model: tabModel, + let controller = HomeViewController.loadFromStoryboard(homePageConfiguration: homePageConfiguration, + model: tabModel, favoritesViewModel: favoritesViewModel, appSettings: appSettings, syncService: syncService, diff --git a/DuckDuckGo/NewTabPageMessagesModel.swift b/DuckDuckGo/NewTabPageMessagesModel.swift index 7f6ec3e60a..486790bd58 100644 --- a/DuckDuckGo/NewTabPageMessagesModel.swift +++ b/DuckDuckGo/NewTabPageMessagesModel.swift @@ -31,7 +31,7 @@ final class NewTabPageMessagesModel: ObservableObject { private let notificationCenter: NotificationCenter private let pixelFiring: PixelFiring.Type - init(homePageMessagesConfiguration: HomePageMessagesConfiguration = AppDependencyProvider.shared.homePageConfiguration, + init(homePageMessagesConfiguration: HomePageMessagesConfiguration, notificationCenter: NotificationCenter = .default, pixelFiring: PixelFiring.Type = Pixel.self) { self.homePageMessagesConfiguration = homePageMessagesConfiguration diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index fcb7e5907b..e0bb708ab8 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -81,7 +81,14 @@ struct NewTabPageView: View { // MARK: - Preview #Preview("Regular") { - NewTabPageView(messagesModel: NewTabPageMessagesModel(), favoritesModel: FavoritesModel()) + NewTabPageView( + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesModel: FavoritesModel() + ) } #Preview("With message") { diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index b44b93a78e..f8f66c5113 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -21,8 +21,8 @@ import SwiftUI final class NewTabPageViewController: UIHostingController, NewTabPage { - init() { - let newTabPageView = NewTabPageView(messagesModel: NewTabPageMessagesModel(), + init(homePageMessagesConfiguration: HomePageMessagesConfiguration) { + let newTabPageView = NewTabPageView(messagesModel: NewTabPageMessagesModel(homePageMessagesConfiguration: homePageMessagesConfiguration), favoritesModel: FavoritesModel()) super.init(rootView: newTabPageView) } diff --git a/DuckDuckGo/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessagingClient.swift index 0925657a2d..da01ddd6ef 100644 --- a/DuckDuckGo/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessagingClient.swift @@ -18,6 +18,7 @@ // import Common +import Configuration import Foundation import Core import BackgroundTasks @@ -25,42 +26,104 @@ import BrowserServicesKit import Persistence import Bookmarks import RemoteMessaging -import NetworkProtection -import Subscription -struct RemoteMessagingClient { +final class RemoteMessagingClient: RemoteMessagingProcessing { - private static let endpoint: URL = { + struct Constants { + static let backgroundRefreshTaskIdentifier = "com.duckduckgo.app.remoteMessageRefresh" + static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 60 * 4 + static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! #else - URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/ios-config.json")! + URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/ios-config.json")! #endif - }() + }() + } + + let endpoint: URL = Constants.endpoint + let configFetcher: RemoteMessagingConfigFetching + let configMatcherProvider: RemoteMessagingConfigMatcherProviding + let store: RemoteMessagingStoring + let remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + + convenience init( + bookmarksDatabase: CoreDataDatabase, + appSettings: AppSettings, + internalUserDecider: InternalUserDecider, + configurationStore: ConfigurationStoring, + database: CoreDataDatabase, + notificationCenter: NotificationCenter = .default, + errorEvents: EventMapping?, + remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + ) { + let provider = RemoteMessagingConfigMatcherProvider( + bookmarksDatabase: bookmarksDatabase, + appSettings: appSettings, + internalUserDecider: internalUserDecider + ) + let configFetcher = RemoteMessagingConfigFetcher( + configurationFetcher: ConfigurationFetcher(store: configurationStore, urlSession: .session(), log: .remoteMessaging, eventMapping: nil), + configurationStore: configurationStore + ) + let remoteMessagingStore = RemoteMessagingStore( + database: database, + notificationCenter: notificationCenter, + errorEvents: errorEvents, + remoteMessagingAvailabilityProvider: remoteMessagingAvailabilityProvider, + log: .remoteMessaging + ) + self.init( + configMatcherProvider: provider, + configFetcher: configFetcher, + store: remoteMessagingStore, + remoteMessagingAvailabilityProvider: remoteMessagingAvailabilityProvider + ) + } + + init( + configMatcherProvider: RemoteMessagingConfigMatcherProviding, + configFetcher: RemoteMessagingConfigFetching, + store: RemoteMessagingStoring, + remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding + ) { + self.configMatcherProvider = configMatcherProvider + self.configFetcher = configFetcher + self.store = store + self.remoteMessagingAvailabilityProvider = remoteMessagingAvailabilityProvider + } @UserDefaultsWrapper(key: .lastRemoteMessagingRefreshDate, defaultValue: .distantPast) static private var lastRemoteMessagingRefreshDate: Date +} - struct Constants { - static let backgroundRefreshTaskIdentifier = "com.duckduckgo.app.remoteMessageRefresh" - static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 60 * 4 - } +// MARK: - Background Refresh + +extension RemoteMessagingClient { static private var shouldRefresh: Bool { - return Date().timeIntervalSince(Self.lastRemoteMessagingRefreshDate) > Constants.minimumConfigurationRefreshInterval + Date().timeIntervalSince(Self.lastRemoteMessagingRefreshDate) > Constants.minimumConfigurationRefreshInterval } - static func registerBackgroundRefreshTaskHandler( - bookmarksDatabase: CoreDataDatabase, - favoritesDisplayMode: @escaping @autoclosure () -> FavoritesDisplayMode - ) { + func registerBackgroundRefreshTaskHandler() { + let provider = configMatcherProvider + let fetcher = configFetcher + let remoteMessagingAvailabilityProvider = remoteMessagingAvailabilityProvider + let store = store + BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundRefreshTaskIdentifier, using: nil) { task in - guard shouldRefresh else { + guard Self.shouldRefresh else { task.setTaskCompleted(success: true) - scheduleBackgroundRefreshTask() + Self.scheduleBackgroundRefreshTask() return } - backgroundRefreshTaskHandler(bgTask: task, bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: favoritesDisplayMode()) + let client = RemoteMessagingClient( + configMatcherProvider: provider, + configFetcher: fetcher, + store: store, + remoteMessagingAvailabilityProvider: remoteMessagingAvailabilityProvider + ) + Self.backgroundRefreshTaskHandler(bgTask: task, client: client) } } @@ -86,15 +149,13 @@ struct RemoteMessagingClient { #endif } - static func backgroundRefreshTaskHandler( - bgTask: BGTask, - bookmarksDatabase: CoreDataDatabase, - favoritesDisplayMode: @escaping @autoclosure () -> FavoritesDisplayMode - ) { + static func backgroundRefreshTaskHandler(bgTask: BGTask, client: RemoteMessagingClient) { let fetchAndProcessTask = Task { do { - try await Self.fetchAndProcess(bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: favoritesDisplayMode()) - Self.lastRemoteMessagingRefreshDate = Date() + if client.remoteMessagingAvailabilityProvider.isRemoteMessagingAvailable { + try await client.fetchAndProcess(using: client.store) + Self.lastRemoteMessagingRefreshDate = Date() + } scheduleBackgroundRefreshTask() bgTask.setTaskCompleted(success: true) } catch { @@ -108,150 +169,4 @@ struct RemoteMessagingClient { bgTask.setTaskCompleted(success: false) } } - - /// Convenience function - static func fetchAndProcess(bookmarksDatabase: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode) async throws { - - var bookmarksCount = 0 - var favoritesCount = 0 - let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) - context.performAndWait { - let displayedFavoritesFolder = BookmarkUtils.fetchFavoritesFolder(withUUID: favoritesDisplayMode.displayedFolder.rawValue, in: context)! - - let bookmarksCountRequest = BookmarkEntity.fetchRequest() - bookmarksCountRequest.predicate = NSPredicate( - format: "SUBQUERY(%K, $x, $x CONTAINS %@).@count == 0 AND %K == false AND %K == false AND (%K == NO OR %K == nil)", - #keyPath(BookmarkEntity.favoriteFolders), - displayedFavoritesFolder, - #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.isPendingDeletion), - #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) - bookmarksCount = (try? context.count(for: bookmarksCountRequest)) ?? 0 - - let favoritesCountRequest = BookmarkEntity.fetchRequest() - favoritesCountRequest.predicate = NSPredicate(format: "%K CONTAINS %@ AND %K == false AND %K == false AND (%K == NO OR %K == nil)", - #keyPath(BookmarkEntity.favoriteFolders), - displayedFavoritesFolder, - #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.isPendingDeletion), - #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) - favoritesCount = (try? context.count(for: favoritesCountRequest)) ?? 0 - } - - let isWidgetInstalled = await AppDependencyProvider.shared.appSettings.isWidgetInstalled() - - try await Self.fetchAndProcess(bookmarksCount: bookmarksCount, - favoritesCount: favoritesCount, - isWidgetInstalled: isWidgetInstalled) - } - - // swiftlint:disable:next function_body_length - static private func fetchAndProcess(bookmarksCount: Int, - favoritesCount: Int, - remoteMessagingStore: RemoteMessagingStore = AppDependencyProvider.shared.remoteMessagingStore, - statisticsStore: StatisticsStore = StatisticsUserDefaults(), - variantManager: VariantManager = DefaultVariantManager(), - isWidgetInstalled: Bool) async throws { - - let result = await Self.fetchRemoteMessages(remoteMessageRequest: RemoteMessageRequest(endpoint: endpoint)) - - switch result { - case .success(let statusResponse): - os_log("Successfully fetched remote messages", log: .remoteMessaging, type: .debug) - - let isPrivacyProSubscriber = AppDependencyProvider.shared.subscriptionManager.accountManager.isUserAuthenticated - let canPurchase = AppDependencyProvider.shared.subscriptionManager.canPurchase - - let activationDateStore = DefaultVPNActivationDateStore() - let daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1 - - var privacyProDaysSinceSubscribed: Int = -1 - var privacyProDaysUntilExpiry: Int = -1 - var privacyProPurchasePlatform: String? - var privacyProIsActive: Bool = false - var privacyProIsExpiring: Bool = false - var privacyProIsExpired: Bool = false - let surveyActionMapper: DefaultRemoteMessagingSurveyURLBuilder - - if let accessToken = AppDependencyProvider.shared.subscriptionManager.accountManager.accessToken { - let subscriptionResult = await AppDependencyProvider.shared.subscriptionManager.subscriptionEndpointService.getSubscription( - accessToken: accessToken - ) - - if case let .success(subscription) = subscriptionResult { - privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 - privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 - privacyProPurchasePlatform = subscription.platform.rawValue - - switch subscription.status { - case .autoRenewable, .gracePeriod: - privacyProIsActive = true - case .notAutoRenewable: - privacyProIsExpiring = true - case .expired, .inactive: - privacyProIsExpired = true - case .unknown: - break // Not supported in RMF - } - - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, subscription: subscription) - } else { - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, subscription: nil) - } - } else { - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, subscription: nil) - } - - let dismissedMessageIds = remoteMessagingStore.fetchDismissedRemoteMessageIds() - - let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher( - appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, - variantManager: variantManager, - isInternalUser: AppDependencyProvider.shared.internalUserDecider.isInternalUser), - userAttributeMatcher: UserAttributeMatcher(statisticsStore: statisticsStore, - variantManager: variantManager, - bookmarksCount: bookmarksCount, - favoritesCount: favoritesCount, - appTheme: AppUserDefaults().currentThemeName.rawValue, - isWidgetInstalled: isWidgetInstalled, - daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled, - isPrivacyProEligibleUser: canPurchase, - isPrivacyProSubscriber: isPrivacyProSubscriber, - privacyProDaysSinceSubscribed: privacyProDaysSinceSubscribed, - privacyProDaysUntilExpiry: privacyProDaysUntilExpiry, - privacyProPurchasePlatform: privacyProPurchasePlatform, - isPrivacyProSubscriptionActive: privacyProIsActive, - isPrivacyProSubscriptionExpiring: privacyProIsExpiring, - isPrivacyProSubscriptionExpired: privacyProIsExpired, - dismissedMessageIds: dismissedMessageIds), - percentileStore: RemoteMessagingPercentileUserDefaultsStore(userDefaults: .standard), - surveyActionMapper: surveyActionMapper, - dismissedMessageIds: dismissedMessageIds - ) - - let processor = RemoteMessagingConfigProcessor(remoteMessagingConfigMatcher: remoteMessagingConfigMatcher) - let config = remoteMessagingStore.fetchRemoteMessagingConfig() - - if let processorResult = processor.process(jsonRemoteMessagingConfig: statusResponse, - currentConfig: config) { - remoteMessagingStore.saveProcessedResult(processorResult) - } - case .failure(let error): - os_log("Failed to fetch remote messages", log: .remoteMessaging, type: .error) - throw error - } - } - - static func fetchRemoteMessages(remoteMessageRequest: RemoteMessageRequest) async -> Result { - return await withCheckedContinuation { continuation in - remoteMessageRequest.getRemoteMessage(completionHandler: { result in - switch result { - case .success(let response): - continuation.resume(returning: .success(response)) - case .failure(let error): - continuation.resume(returning: .failure(error)) - } - }) - } - } } diff --git a/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift new file mode 100644 index 0000000000..ef47b85c6c --- /dev/null +++ b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift @@ -0,0 +1,142 @@ +// +// RemoteMessagingConfigMatcherProvider.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import Core +import Foundation +import BrowserServicesKit +import Persistence +import Bookmarks +import RemoteMessaging +import NetworkProtection +import Subscription + +extension DefaultVPNActivationDateStore: VPNActivationDateProviding {} + +final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherProviding { + + init( + bookmarksDatabase: CoreDataDatabase, + appSettings: AppSettings, + internalUserDecider: InternalUserDecider + ) { + self.bookmarksDatabase = bookmarksDatabase + self.appSettings = appSettings + self.internalUserDecider = internalUserDecider + } + + let bookmarksDatabase: CoreDataDatabase + let appSettings: AppSettings + let internalUserDecider: InternalUserDecider + + // swiftlint:disable:next function_body_length + func refreshConfigMatcher(using store: RemoteMessagingStoring) async -> RemoteMessagingConfigMatcher { + + var bookmarksCount = 0 + var favoritesCount = 0 + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + context.performAndWait { + bookmarksCount = BookmarkUtils.numberOfBookmarks(in: context) + favoritesCount = BookmarkUtils.numberOfFavorites(for: appSettings.favoritesDisplayMode, in: context) + } + + let statisticsStore = StatisticsUserDefaults() + let variantManager = DefaultVariantManager() + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager + + let isPrivacyProSubscriber = subscriptionManager.accountManager.isUserAuthenticated + let isPrivacyProEligibleUser = subscriptionManager.canPurchase + + let activationDateStore = DefaultVPNActivationDateStore() + let daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1 + + var privacyProDaysSinceSubscribed: Int = -1 + var privacyProDaysUntilExpiry: Int = -1 + var privacyProPurchasePlatform: String? + var isPrivacyProSubscriptionActive: Bool = false + var isPrivacyProSubscriptionExpiring: Bool = false + var isPrivacyProSubscriptionExpired: Bool = false + let surveyActionMapper: DefaultRemoteMessagingSurveyURLBuilder + + if let accessToken = subscriptionManager.accountManager.accessToken { + let subscriptionResult = await subscriptionManager.subscriptionEndpointService.getSubscription( + accessToken: accessToken + ) + + if case let .success(subscription) = subscriptionResult { + privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 + privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 + privacyProPurchasePlatform = subscription.platform.rawValue + + switch subscription.status { + case .autoRenewable, .gracePeriod: + isPrivacyProSubscriptionActive = true + case .notAutoRenewable: + isPrivacyProSubscriptionExpiring = true + case .expired, .inactive: + isPrivacyProSubscriptionExpired = true + case .unknown: + break // Not supported in RMF + } + + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( + statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: subscription) + } else { + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( + statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) + } + } else { + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( + statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) + } + + let dismissedMessageIds = store.fetchDismissedRemoteMessageIDs() + + return RemoteMessagingConfigMatcher( + appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, + variantManager: variantManager, + isInternalUser: internalUserDecider.isInternalUser), + userAttributeMatcher: UserAttributeMatcher(statisticsStore: statisticsStore, + variantManager: variantManager, + bookmarksCount: bookmarksCount, + favoritesCount: favoritesCount, + appTheme: appSettings.currentThemeName.rawValue, + isWidgetInstalled: await appSettings.isWidgetInstalled(), + daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled, + isPrivacyProEligibleUser: isPrivacyProEligibleUser, + isPrivacyProSubscriber: isPrivacyProSubscriber, + privacyProDaysSinceSubscribed: privacyProDaysSinceSubscribed, + privacyProDaysUntilExpiry: privacyProDaysUntilExpiry, + privacyProPurchasePlatform: privacyProPurchasePlatform, + isPrivacyProSubscriptionActive: isPrivacyProSubscriptionActive, + isPrivacyProSubscriptionExpiring: isPrivacyProSubscriptionExpiring, + isPrivacyProSubscriptionExpired: isPrivacyProSubscriptionExpired, + dismissedMessageIds: dismissedMessageIds), + percentileStore: RemoteMessagingPercentileUserDefaultsStore(keyValueStore: UserDefaults.standard), + surveyActionMapper: surveyActionMapper, + dismissedMessageIds: dismissedMessageIds + ) + } +} diff --git a/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift b/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift deleted file mode 100644 index 1ea51ddc39..0000000000 --- a/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// RemoteMessagingSurveyURLBuilder.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import BrowserServicesKit -import RemoteMessaging -import Core -import Common -import Subscription - -struct DefaultRemoteMessagingSurveyURLBuilder: RemoteMessagingSurveyActionMapping { - - private let statisticsStore: StatisticsStore - private let vpnActivationDateStore: VPNActivationDateStore - private let subscription: Subscription? - - init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), - vpnActivationDateStore: VPNActivationDateStore = DefaultVPNActivationDateStore(), - subscription: Subscription?) { - self.statisticsStore = statisticsStore - self.vpnActivationDateStore = vpnActivationDateStore - self.subscription = subscription - } - - // swiftlint:disable:next cyclomatic_complexity function_body_length - func add(parameters: [RemoteMessagingSurveyActionParameter], to surveyURL: URL) -> URL { - guard var components = URLComponents(string: surveyURL.absoluteString) else { - assertionFailure("Could not build URL components from survey URL") - return surveyURL - } - - var queryItems = components.queryItems ?? [] - - for parameter in parameters { - switch parameter { - case .atb: - if let atb = statisticsStore.atb { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: atb)) - } - case .atbVariant: - if let variant = statisticsStore.variant { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: variant)) - } - case .osVersion: - queryItems.append(URLQueryItem(name: parameter.rawValue, value: AppVersion.shared.osVersion)) - case .appVersion: - queryItems.append(URLQueryItem(name: parameter.rawValue, value: AppVersion.shared.versionAndBuildNumber)) - case .hardwareModel: - let model = hardwareModel().addingPercentEncoding(withAllowedCharacters: .alphanumerics) - queryItems.append(URLQueryItem(name: parameter.rawValue, value: model)) - case .daysInstalled: - if let installDate = statisticsStore.installDate, - let daysSinceInstall = Calendar.current.numberOfDaysBetween(installDate, and: Date()) { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSinceInstall))) - } - case .privacyProStatus: - switch subscription?.status { - case .autoRenewable: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "auto_renewable")) - case .notAutoRenewable: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "not_auto_renewable")) - case .gracePeriod: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "grace_period")) - case .inactive: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "inactive")) - case .expired: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "expired")) - case .unknown: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "unknown")) - case nil: break - } - case .privacyProPlatform: - - switch subscription?.platform { - case .apple: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "apple")) - case .google: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "google")) - case .stripe: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "stripe")) - case .unknown: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "unknown")) - case nil: break - } - case .privacyProBilling: - switch subscription?.billingPeriod { - case .monthly: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "monthly")) - case .yearly: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "yearly")) - case .unknown: queryItems.append(URLQueryItem(name: parameter.rawValue, value: "unknown")) - case nil: break - } - - case .privacyProDaysSincePurchase: - if let startDate = subscription?.startedAt, - let daysSincePurchase = Calendar.current.numberOfDaysBetween(startDate, and: Date()) { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSincePurchase))) - } - case .privacyProDaysUntilExpiry: - if let expiryDate = subscription?.expiresOrRenewsAt, - let daysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: expiryDate) { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysUntilExpiry))) - } - case .vpnFirstUsed: - if let vpnFirstUsed = vpnActivationDateStore.daysSinceActivation() { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: vpnFirstUsed))) - } - case .vpnLastUsed: - if let vpnLastUsed = vpnActivationDateStore.daysSinceLastActive() { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: vpnLastUsed))) - } - } - } - - components.queryItems = queryItems - - return components.url ?? surveyURL - } - - private func hardwareModel() -> String { - var systemInfo = utsname() - uname(&systemInfo) - - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } - - return identifier - } - -} diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index 68711cf742..7a0a327087 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -32,8 +32,6 @@ class MockDependencyProvider: DependencyProvider { var variantManager: VariantManager var featureFlagger: FeatureFlagger var internalUserDecider: InternalUserDecider - var remoteMessagingStore: RemoteMessagingStore - var homePageConfiguration: HomePageConfiguration var storageCache: StorageCache var voiceSearchHelper: VoiceSearchHelperProtocol var downloadManager: DownloadManager @@ -56,8 +54,6 @@ class MockDependencyProvider: DependencyProvider { variantManager = defaultProvider.variantManager featureFlagger = defaultProvider.featureFlagger internalUserDecider = defaultProvider.internalUserDecider - remoteMessagingStore = defaultProvider.remoteMessagingStore - homePageConfiguration = defaultProvider.homePageConfiguration storageCache = defaultProvider.storageCache voiceSearchHelper = defaultProvider.voiceSearchHelper downloadManager = defaultProvider.downloadManager