diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index df96f2f78a..aa6ee329fc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1713,6 +1713,10 @@ 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; + 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; + 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; + 9F0660782BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */; }; + 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; @@ -1747,6 +1751,14 @@ 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; + 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; + 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; + 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; + 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; + 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; @@ -3474,6 +3486,8 @@ 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; + 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandlerTests.swift; sourceTree = ""; }; + 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelCapturedParameters.swift; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelMock.swift; sourceTree = ""; }; @@ -3491,6 +3505,10 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; + 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionPixelHandler.swift; sourceTree = ""; }; + 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; + 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; + 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManagerTests.swift; sourceTree = ""; }; 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; @@ -4516,6 +4534,8 @@ isa = PBXGroup; children = ( 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, + 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */, + 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */, ); path = Subscription; sourceTree = ""; @@ -6252,6 +6272,7 @@ B683097A274DCFE3004B46BB /* Database */, AAEC74B92642E66600C2EFBC /* Extensions */, 4BA1A6CE258BF58C00F6F690 /* FileSystem */, + 9F0660752BECC7E700B8EEF1 /* Pixel */, B6AE74322609AFBB005B9B1A /* Progress */, B698E5032908011E00A746A8 /* AppKitPrivateMethodsAvailabilityTests.swift */, B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */, @@ -6303,6 +6324,14 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F0660752BECC7E700B8EEF1 /* Pixel */ = { + isa = PBXGroup; + children = ( + 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */, + ); + path = Pixel; + sourceTree = ""; + }; 9F0FFFB62BCCAE80007C87DD /* Mocks */ = { isa = PBXGroup; children = ( @@ -6313,6 +6342,23 @@ path = Mocks; sourceTree = ""; }; + 9F64345F2BEC82A000D2D8A0 /* Attribution */ = { + isa = PBXGroup; + children = ( + 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */, + ); + path = Attribution; + sourceTree = ""; + }; + 9F64346E2BECB9FB00D2D8A0 /* Subscriptions */ = { + isa = PBXGroup; + children = ( + 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */, + 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */, + ); + path = Subscriptions; + sourceTree = ""; + }; 9F872D9B2B9058B000138637 /* Extensions */ = { isa = PBXGroup; children = ( @@ -6635,6 +6681,7 @@ 858A798626A99D9000A75A42 /* SecureVault */, B6DA440F2616C0F200DD1EC2 /* Statistics */, AA63744E24C9BB4A00AB2AC4 /* Suggestions */, + 9F64346E2BECB9FB00D2D8A0 /* Subscriptions */, AA92ACAE24EFE1F5005F41C9 /* Tab */, AAC9C01224CAFBB700AD1325 /* TabBar */, B6CA4822298CDC0B0067ECCE /* TabExtensionsTests */, @@ -6900,6 +6947,7 @@ AA86491324D831B9001BABEE /* Common */ = { isa = PBXGroup; children = ( + 9F64345F2BEC82A000D2D8A0 /* Attribution */, 4B67743D255DBEEA00025BD8 /* Database */, AADC60E92493B305008F8EF7 /* Extensions */, 4BA1A691258B06F600F6F690 /* FileSystem */, @@ -9522,6 +9570,7 @@ 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, + 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9957,6 +10006,7 @@ B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, + 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, 3706FBED293F65D500E42796 /* TabCollection.swift in Sources */, B6C0BB6B29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, @@ -10065,6 +10115,7 @@ 31A83FB62BE28D7D00F74E67 /* UserText+DBP.swift in Sources */, 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 3706FC31293F65D500E42796 /* PermissionButton.swift in Sources */, + 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, @@ -10329,6 +10380,7 @@ 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */, 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */, + 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, 3706FE20293F661700E42796 /* CLLocationManagerMock.swift in Sources */, B6656E0E2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, B6C843DB2BA1CAB6006FDEC3 /* FilePresenterTests.swift in Sources */, @@ -10344,6 +10396,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, @@ -10497,6 +10550,7 @@ 5681ED442BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */, B60C6F8529B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, 567DA94629E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, + 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, 3706FE82293F661700E42796 /* MockStatisticsStore.swift in Sources */, 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, @@ -10985,6 +11039,7 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, + 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, @@ -11094,6 +11149,7 @@ B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, + 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, @@ -11151,6 +11207,7 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, + 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11594,6 +11651,7 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, + 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */, 4B9292BC2667103100AD2C21 /* BookmarkSidebarTreeControllerTests.swift in Sources */, @@ -11720,6 +11778,7 @@ 1D8C2FED2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, + 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */, @@ -11755,6 +11814,7 @@ B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */, 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, 1D8C2FF02B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */, + 9F0660782BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, 1D3B1AC429378953006F4388 /* BWResponseTests.swift in Sources */, B693955F26F1C17F0015B914 /* DownloadListCoordinatorTests.swift in Sources */, 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, @@ -12842,7 +12902,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = "144.0.7-3"; + version = "144.0.7-4"; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cb1323b9af..e9bb970531 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" : "ada5f68970f098b3230dbd80a25cd048a606ac12", - "version" : "144.0.7-3" + "revision" : "43db1d59455246547fc4ea3998f07751dfa77166", + "version" : "144.0.7-4" } }, { diff --git a/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift b/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift new file mode 100644 index 0000000000..0a21678f79 --- /dev/null +++ b/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift @@ -0,0 +1,97 @@ +// +// AttributionPixelHandler.swift +// +// 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 Foundation +import PixelKit + +// A type that send pixels that needs attributions parameters. +protocol AttributionPixelHandler { + func fireAttributionPixel( + event: PixelKit.Event, + frequency: PixelKit.Frequency, + origin: String?, + additionalParameters: [String: String]? + ) +} + +final class GenericAttributionPixelHandler: AttributionPixelHandler { + enum Parameters { + static let origin = "origin" + static let locale = "locale" + } + + private let fireRequest: FireRequest + private let locale: Locale + + /// Creates an instance with the specified fire request, origin provider and locale. + /// - Parameters: + /// - fireRequest: A function for sending the Pixel request. + /// - locale: The locale of the device. + init( + fireRequest: @escaping FireRequest = PixelKit.fire, + locale: Locale = .current + ) { + self.fireRequest = fireRequest + self.locale = locale + } + + func fireAttributionPixel( + event: PixelKit.Event, + frequency: PixelKit.Frequency, + origin: String?, + additionalParameters: [String: String]? + ) { + fireRequest( + event, + frequency, + [:], + self.parameters(additionalParameters, withOrigin: origin, locale: locale.identifier), + nil, + nil, + true, { _, _ in } + ) + } +} + +// MARK: - Parameter + +private extension GenericAttributionPixelHandler { + func parameters(_ parameters: [String: String]?, withOrigin origin: String?, locale: String) -> [String: String] { + var parameters = parameters ?? [:] + parameters[Self.Parameters.locale] = locale + if let origin { + parameters[Self.Parameters.origin] = origin + } + return parameters + } +} + +// MARK: - FireRequest + +extension GenericAttributionPixelHandler { + typealias FireRequest = ( + _ event: PixelKit.Event, + _ frequency: PixelKit.Frequency, + _ headers: [String: String], + _ parameters: [String: String]?, + _ error: Error?, + _ allowedQueryReservedCharacters: CharacterSet?, + _ includeAppVersionParameter: Bool, + _ onComplete: @escaping (Bool, Error?) -> Void + ) -> Void +} diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 07683d29cc..db79eac20f 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -581,4 +581,5 @@ extension URL { return false } } + } diff --git a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift index b26f411d9c..b9d05691e7 100644 --- a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift +++ b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift @@ -20,72 +20,29 @@ import Foundation import PixelKit /// A type that handles Pixels for acquisition attributions. -protocol AttributionsPixelHandler: AnyObject { +protocol InstallationAttributionsPixelHandler: AnyObject { /// Fire the Pixel to track the App install. func fireInstallationAttributionPixel() } -final class InstallationAttributionPixelHandler: AttributionsPixelHandler { - enum Parameters { - static let origin = "origin" - static let locale = "locale" - } - - private let fireRequest: FireRequest +final class AppInstallationAttributionPixelHandler: InstallationAttributionsPixelHandler { private let originProvider: AttributionOriginProvider - private let locale: Locale + private let decoratedAttributionPixelHandler: AttributionPixelHandler - /// Creates an instance with the specified fire request, origin provider and locale. - /// - Parameters: - /// - fireRequest: A function for sending the Pixel request. - /// - originProvider: A provider for the origin used to track the acquisition funnel. - /// - locale: The locale of the device. init( - fireRequest: @escaping FireRequest = PixelKit.fire, originProvider: AttributionOriginProvider = AttributionOriginFileProvider(), - locale: Locale = .current + attributionPixelHandler: AttributionPixelHandler = GenericAttributionPixelHandler() ) { - self.fireRequest = fireRequest self.originProvider = originProvider - self.locale = locale + decoratedAttributionPixelHandler = attributionPixelHandler } func fireInstallationAttributionPixel() { - fireRequest( - GeneralPixel.installationAttribution, - .legacyInitial, - [:], - additionalParameters(origin: originProvider.origin, locale: locale.identifier), - nil, - nil, - true, { _, _ in } + decoratedAttributionPixelHandler.fireAttributionPixel( + event: GeneralPixel.installationAttribution, + frequency: .legacyInitial, + origin: originProvider.origin, + additionalParameters: nil ) } } - -// MARK: - Parameter - -private extension InstallationAttributionPixelHandler { - func additionalParameters(origin: String?, locale: String) -> [String: String] { - var dictionary = [Self.Parameters.locale: locale] - if let origin { - dictionary[Self.Parameters.origin] = origin - } - return dictionary - } -} - -// MARK: - FireRequest - -extension InstallationAttributionPixelHandler { - typealias FireRequest = ( - _ event: PixelKit.Event, - _ frequency: PixelKit.Frequency, - _ headers: [String: String], - _ parameters: [String: String]?, - _ error: Error?, - _ allowedQueryReservedCharacters: CharacterSet?, - _ includeAppVersionParameter: Bool, - _ onComplete: @escaping (Bool, Error?) -> Void - ) -> Void -} diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 744f4b6071..84219ba446 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -30,14 +30,14 @@ final class StatisticsLoader { private let statisticsStore: StatisticsStore private let emailManager: EmailManager - private let attributionPixelHandler: AttributionsPixelHandler + private let attributionPixelHandler: InstallationAttributionsPixelHandler private let parser = AtbParser() private var isAppRetentionRequestInProgress = false init( statisticsStore: StatisticsStore = LocalStatisticsStore(), emailManager: EmailManager = EmailManager(), - attributionPixelHandler: AttributionsPixelHandler = InstallationAttributionPixelHandler() + attributionPixelHandler: InstallationAttributionsPixelHandler = AppInstallationAttributionPixelHandler() ) { self.statisticsStore = statisticsStore self.emailManager = emailManager diff --git a/DuckDuckGo/Statistics/PrivacyProPixel.swift b/DuckDuckGo/Statistics/PrivacyProPixel.swift index d50f06656a..a4093228f6 100644 --- a/DuckDuckGo/Statistics/PrivacyProPixel.swift +++ b/DuckDuckGo/Statistics/PrivacyProPixel.swift @@ -65,6 +65,7 @@ enum PrivacyProPixel: PixelKitEventV2 { case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval case privacyProPurchaseStripeSuccess + case privacyProSuccessfulSubscriptionAttribution // Web pixels case privacyProOfferMonthlyPriceClick case privacyProOfferYearlyPriceClick @@ -110,6 +111,7 @@ enum PrivacyProPixel: PixelKitEventV2 { case .privacyProSubscriptionManagementPlanBilling: return "m_mac_\(appDistribution)_privacy-pro_settings_change-plan-or-billing_click" case .privacyProSubscriptionManagementRemoval: return "m_mac_\(appDistribution)_privacy-pro_settings_remove-from-device_click" case .privacyProPurchaseStripeSuccess: return "m_mac_\(appDistribution)_privacy-pro_app_subscription-purchase_stripe_success" + case .privacyProSuccessfulSubscriptionAttribution: return "m_mac_\(appDistribution)_subscribe" // Web case .privacyProOfferMonthlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_monthly-price_click" case .privacyProOfferYearlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_yearly-price_click" diff --git a/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift b/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift new file mode 100644 index 0000000000..a6361d0427 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift @@ -0,0 +1,46 @@ +// +// SubscriptionAttributionPixelHandler.swift +// +// 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 Foundation +import Subscription + +protocol SubscriptionAttributionPixelHandler: AnyObject { + var origin: String? { get set } + func fireSuccessfulSubscriptionAttributionPixel() +} + +// MARK: - SubscriptionAttributionPixelHandler + +final class PrivacyProSubscriptionAttributionPixelHandler: SubscriptionAttributionPixelHandler { + var origin: String? + private let decoratedAttributionPixelHandler: AttributionPixelHandler + + init(attributionPixelHandler: AttributionPixelHandler = GenericAttributionPixelHandler()) { + decoratedAttributionPixelHandler = attributionPixelHandler + } + + func fireSuccessfulSubscriptionAttributionPixel() { + decoratedAttributionPixelHandler.fireAttributionPixel( + event: PrivacyProPixel.privacyProSuccessfulSubscriptionAttribution, + frequency: .standard, + origin: origin, + additionalParameters: nil + ) + } + +} diff --git a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift new file mode 100644 index 0000000000..66e6aefa04 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift @@ -0,0 +1,66 @@ +// +// SubscriptionRedirectManager.swift +// +// 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 Foundation +import Subscription +import BrowserServicesKit + +protocol SubscriptionRedirectManager: AnyObject { + func redirectURL(for url: URL) -> URL? +} + +final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { + private let featureAvailabiltyProvider: () -> Bool + + init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable) { + self.featureAvailabiltyProvider = featureAvailabiltyProvider + } + + func redirectURL(for url: URL) -> URL? { + guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } + + if url.pathComponents == URL.privacyPro.pathComponents { + let isFeatureAvailable = featureAvailabiltyProvider() + let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts + // Redirect the `/pro` URL to `/subscriptions` URL. If there are any query items in the original URL it appends to the `/subscriptions` URL. + return isPurchasePageRedirectActive ? URL.subscriptionBaseURL.addingQueryItems(from: url) : nil + } + + return nil + } + +} + +private extension URL { + + func addingQueryItems(from url: URL) -> URL { + // If the origin value is of type "do+something" appending the percentEncodedQueryItem crashes the browser as + is replaced by a space. + // Perform encoding on the value to avoid the crash. + guard let queryItems = url.getQueryItems()? + .compactMap({ queryItem -> URLQueryItem? in + guard let value = queryItem.value else { return nil } + let encodedValue = value.percentEncoded(withAllowedCharacters: .urlQueryParameterAllowed) + return URLQueryItem(name: queryItem.name, value: encodedValue) + }) + else { return self } + + return self.appending(percentEncodedQueryItems: queryItems) + } + +} diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index d06cb5f3d3..81f400302c 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -18,13 +18,17 @@ import Navigation import Foundation -import Subscription -import BrowserServicesKit struct RedirectNavigationResponder: NavigationResponder { + private let redirectManager: SubscriptionRedirectManager + + init(redirectManager: SubscriptionRedirectManager = PrivacyProSubscriptionRedirectManager()) { + self.redirectManager = redirectManager + } + func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? { - guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectURL(for: navigationAction.url) else { return .next } + guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectManager.redirectURL(for: navigationAction.url) else { return .next } return .redirect(mainFrame) { navigator in var request = navigationAction.request @@ -33,17 +37,4 @@ struct RedirectNavigationResponder: NavigationResponder { } } - private func redirectURL(for url: URL) -> URL? { - guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } - - if url.pathComponents == URL.privacyPro.pathComponents { - let isFeatureAvailable = DefaultSubscriptionFeatureAvailability().isFeatureAvailable - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false - let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts - - return isPurchasePageRedirectActive ? URL.subscriptionBaseURL : nil - } - - return nil - } } diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index 4d9b38724b..f0e7525820 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -90,6 +90,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.broker = broker } + private let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + + init(subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { + self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + } + struct Handlers { static let getSubscription = "getSubscription" static let setSubscription = "setSubscription" @@ -208,7 +214,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // swiftlint:disable:next function_body_length cyclomatic_complexity func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) struct SubscriptionSelection: Decodable { let id: String @@ -216,6 +221,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let message = original + // Extract the origin from the webview URL to use for attribution pixel. + subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) + if SubscriptionPurchaseEnvironment.current == .appStore { if #available(macOS 12.0, *) { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController @@ -287,6 +295,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { os_log(.info, log: .subscription, "[Purchase] Purchase complete") PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .unique) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure(let error): switch error { @@ -420,6 +429,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.dismiss(progressViewController) PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() return [String: String]() // cannot be nil, the web app expect something back before redirecting the user to the final page } @@ -477,6 +487,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { broker.push(method: method.rawValue, params: params, for: self, into: webView) } + + @MainActor + private func originFrom(originalMessage: WKScriptMessage) -> String? { + let url = originalMessage.webView?.url + return url?.getParameter(named: AttributionParameter.origin) + } } extension MainWindowController { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index ca45e2f839..323f88e898 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-4"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 5c1a60bab9..2a0f9704d3 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-4"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 6f7d2da22e..20769260df 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-4"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Common/Pixel/PixelCapturedParameters.swift b/UnitTests/Common/Pixel/PixelCapturedParameters.swift new file mode 100644 index 0000000000..b73c5b38af --- /dev/null +++ b/UnitTests/Common/Pixel/PixelCapturedParameters.swift @@ -0,0 +1,31 @@ +// +// PixelCapturedParameters.swift +// +// 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 Foundation +import PixelKit + +struct PixelCapturedParameters { + var event: PixelKit.Event? + var frequency: PixelKit.Frequency = .standard + var headers: [String: String] = [:] + var parameters: [String: String]? + var error: Error? + var reservedCharacters: CharacterSet? + var includeAppVersion: Bool? + var onComplete: (Bool, Error?) -> Void = { _, _ in } +} diff --git a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift index 0d217cf769..df475b229b 100644 --- a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift +++ b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift @@ -21,13 +21,13 @@ import PixelKit @testable import DuckDuckGo_Privacy_Browser final class InstallationAttributionPixelHandlerTests: XCTestCase { - private var sut: InstallationAttributionPixelHandler! - private var capturedParams: CapturedParameters! - private var fireRequest: InstallationAttributionPixelHandler.FireRequest! + private var sut: AppInstallationAttributionPixelHandler! + private var capturedParams: PixelCapturedParameters! + private var fireRequest: GenericAttributionPixelHandler.FireRequest! override func setUpWithError() throws { try super.setUpWithError() - capturedParams = CapturedParameters() + capturedParams = PixelCapturedParameters() fireRequest = { event, frequency, headers, parameters, error, reservedCharacters, includeAppVersion, onComplete in self.capturedParams.event = event self.capturedParams.frequency = frequency @@ -49,7 +49,8 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { func testWhenPixelFiresThenNameIsSetToM_Mac_Install() { // GIVEN - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() @@ -61,13 +62,14 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { func testWhenPixelFiresThenLanguageCodeIsSet() { // GIVEN let locale = Locale(identifier: "hu-HU") - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: locale) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "hu-HU") + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "hu-HU") } func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { @@ -75,14 +77,15 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { let origin = "app_search" let locale = Locale(identifier: "en-US") let originProvider = MockAttributionOriginProvider(origin: origin) - sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: originProvider, attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin], origin) - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") } func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { @@ -90,19 +93,20 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { let origin: String? = nil let locale = Locale(identifier: "en-US") let originProvider = MockAttributionOriginProvider(origin: origin) - sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) - + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: originProvider, attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertNil(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin]) - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + XCTAssertNil(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") } func testWhenPixelFiresThenAddAppVersionIsTrueAndFrequencyIsLegacyInitial() { // GIVEN - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() @@ -112,18 +116,3 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { XCTAssertEqual(capturedParams.frequency, .legacyInitial) } } - -extension InstallationAttributionPixelHandlerTests { - - struct CapturedParameters { - var event: PixelKit.Event? - var frequency: PixelKit.Frequency = .standard - var headers: [String: String] = [:] - var parameters: [String: String]? - var error: Error? - var reservedCharacters: CharacterSet? - var includeAppVersion: Bool? - var onComplete: (Bool, Error?) -> Void = { _, _ in } - } - -} diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift index 05e91bff49..bd07f8f56d 100644 --- a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription @testable import DuckDuckGo_Privacy_Browser final class MockAttributionOriginProvider: AttributionOriginProvider { diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift index a32ee60a30..cc6aa508a1 100644 --- a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift @@ -19,7 +19,7 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser -final class MockAttributionsPixelHandler: AttributionsPixelHandler { +final class MockAttributionsPixelHandler: InstallationAttributionsPixelHandler { private(set) var fireInstallationAttributionPixelCount = 0 private(set) var didCallFireInstallationAttributionPixel = false diff --git a/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift b/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift new file mode 100644 index 0000000000..8bc003fe07 --- /dev/null +++ b/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift @@ -0,0 +1,124 @@ +// +// SubscriptionAttributionPixelHandlerTests.swift +// +// 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 XCTest +import PixelKit +@testable import DuckDuckGo_Privacy_Browser + +final class SubscriptionAttributionPixelHandlerTests: XCTestCase { + private var sut: PrivacyProSubscriptionAttributionPixelHandler! + private var capturedParams: PixelCapturedParameters! + private var fireRequest: GenericAttributionPixelHandler.FireRequest! + + override func setUpWithError() throws { + try super.setUpWithError() + capturedParams = PixelCapturedParameters() + fireRequest = { event, frequency, headers, parameters, error, reservedCharacters, includeAppVersion, onComplete in + self.capturedParams.event = event + self.capturedParams.frequency = frequency + self.capturedParams.headers = headers + self.capturedParams.parameters = parameters + self.capturedParams.error = error + self.capturedParams.reservedCharacters = reservedCharacters + self.capturedParams.includeAppVersion = includeAppVersion + self.capturedParams.onComplete = onComplete + } + } + + override func tearDownWithError() throws { + capturedParams = nil + fireRequest = nil + sut = nil + try super.tearDownWithError() + } + + func testWhenPixelFiresThenNameIsSetToM_Mac_DistributionType_Subscribe() { + // GIVEN + #if APPSTORE + let expectedPixelName = "m_mac_store_subscribe" + #else + let expectedPixelName = "m_mac_direct_subscribe" + #endif + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.event?.name, expectedPixelName) + } + + func testWhenPixelFiresThenLanguageCodeIsSet() { + // GIVEN + let locale = Locale(identifier: "hu-HU") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "hu-HU") + } + + func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { + // GIVEN + let origin = "app_search" + let locale = Locale(identifier: "en-US") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + sut.origin = origin + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { + // GIVEN + let origin: String? = nil + let locale = Locale(identifier: "en-US") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + sut.origin = origin + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertNil(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresThenAddAppVersionIsTrueAndFrequencyIsStandard() { + // GIVEN + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.includeAppVersion, true) + XCTAssertEqual(capturedParams.frequency, .standard) + } +} diff --git a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift new file mode 100644 index 0000000000..814abc7a42 --- /dev/null +++ b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift @@ -0,0 +1,61 @@ +// +// SubscriptionRedirectManagerTests.swift +// +// 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 XCTest +import Subscription +@testable import DuckDuckGo_Privacy_Browser + +final class SubscriptionRedirectManagerTests: XCTestCase { + private var sut: PrivacyProSubscriptionRedirectManager! + + override func setUpWithError() throws { + try super.setUpWithError() + sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true) + SubscriptionPurchaseEnvironment.canPurchase = true + } + + override func tearDownWithError() throws { + sut = nil + try super.tearDownWithError() + } + + func testWhenURLIsPrivacyProAndHasOriginQueryParameterThenRedirectToSubscriptionBaseURLAndAppendQueryParameter() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro?origin=test")) + let expectedURL = URL.subscriptionBaseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) + + // WHEN + let result = sut.redirectURL(for: url) + + // THEN + XCTAssertEqual(result, expectedURL) + } + + func testWhenURLIsPrivacyProAndDoesNotHaveOriginQueryParameterThenRedirectToSubscriptionBaseURL() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro")) + let expectedURL = URL.subscriptionBaseURL + + // WHEN + let result = sut.redirectURL(for: url) + + // THEN + XCTAssertEqual(result, expectedURL) + } + +}