diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7c07079afb..a7ac0500a2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1164,11 +1164,7 @@ 4B379C2227BDBA29008A968E /* LocalAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B379C2127BDBA29008A968E /* LocalAuthenticationService.swift */; }; 4B379C2427BDE1B0008A968E /* FlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B379C2327BDE1B0008A968E /* FlatButton.swift */; }; 4B37EE5F2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */; }; - 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */; }; - 4B37EE632B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */; }; - 4B37EE722B4CFEE400A89A61 /* SurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */; }; 4B37EE732B4CFF0800A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */; }; - 4B37EE742B4CFF0A00A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */; }; 4B39AAF627D9B2C700A73FD5 /* NSStackViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */; }; 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */; }; 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F641D27A8D3BD00E0C118 /* BrowserProfileTests.swift */; }; @@ -1383,8 +1379,6 @@ 4BBDEE9328FC14760092FAA6 /* ConnectBitwardenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBDEE8F28FC14760092FAA6 /* ConnectBitwardenViewModel.swift */; }; 4BBDEE9428FC14760092FAA6 /* ConnectBitwardenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBDEE9028FC14760092FAA6 /* ConnectBitwardenViewController.swift */; }; 4BBE0AA727B9B027003B37A8 /* PopUpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE0AA627B9B027003B37A8 /* PopUpButton.swift */; }; - 4BBEE8DE2BFEDE3E00E5E111 /* SurveyRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */; }; - 4BBEE8DF2BFEE07D00E5E111 /* survey-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BCF15E92ABB99470083F6DF /* survey-messages.json */; }; 4BBF0915282DD40100EE1418 /* TemporaryFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0914282DD40100EE1418 /* TemporaryFileHandler.swift */; }; 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0916282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift */; }; 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF09222830812900EE1418 /* FileSystemDSL.swift */; }; @@ -1394,16 +1388,8 @@ 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */; }; 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4592BA7E17800FC75A1 /* Subscription */; }; 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE45B2BA7E18500FC75A1 /* Subscription */; }; - 4BCF15D72ABB8A110083F6DF /* SurveyRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */; }; - 4BCF15D92ABB8A7F0083F6DF /* SurveyRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */; }; - 4BCF15EC2ABB9AF80083F6DF /* SurveyRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */; }; - 4BCF15ED2ABB9B180083F6DF /* survey-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BCF15E92ABB99470083F6DF /* survey-messages.json */; }; - 4BCF15EE2ABBDBFD0083F6DF /* SurveyRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */; }; - 4BCF15EF2ABBDBFF0083F6DF /* SurveyRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */; }; 4BD18F01283F0BC500058124 /* BookmarksBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD18EFF283F0BC500058124 /* BookmarksBarViewController.swift */; }; 4BD18F05283F151F00058124 /* BookmarksBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */; }; - 4BD57C042AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */; }; - 4BD57C052AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */; }; 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; @@ -3251,8 +3237,6 @@ 4B379C2127BDBA29008A968E /* LocalAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationService.swift; sourceTree = ""; }; 4B379C2327BDE1B0008A968E /* FlatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlatButton.swift; sourceTree = ""; }; 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageRemoteMessagingStorage.swift; sourceTree = ""; }; - 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyURLBuilder.swift; sourceTree = ""; }; - 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageRemoteMessagingRequest.swift; sourceTree = ""; }; 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStackViewExtension.swift; sourceTree = ""; }; 4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManagerExtension.swift; sourceTree = ""; }; 4B3F641D27A8D3BD00E0C118 /* BrowserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserProfileTests.swift; sourceTree = ""; }; @@ -3430,13 +3414,8 @@ 4BBF0916282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryFileHandlerTests.swift; sourceTree = ""; }; 4BBF09222830812900EE1418 /* FileSystemDSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemDSL.swift; sourceTree = ""; }; 4BBF0924283083EC00EE1418 /* FileSystemDSLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemDSLTests.swift; sourceTree = ""; }; - 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessaging.swift; sourceTree = ""; }; - 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessage.swift; sourceTree = ""; }; - 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessageTests.swift; sourceTree = ""; }; - 4BCF15E92ABB99470083F6DF /* survey-messages.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "survey-messages.json"; sourceTree = ""; }; 4BD18EFF283F0BC500058124 /* BookmarksBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarViewController.swift; sourceTree = ""; }; 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; - 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRedditSessionWorkaround.swift; sourceTree = ""; }; @@ -5288,11 +5267,7 @@ 4B37EE5B2B4CFC3C00A89A61 /* Surveys */ = { isa = PBXGroup; children = ( - 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */, 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */, - 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */, - 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */, - 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */, ); path = Surveys; sourceTree = ""; @@ -5949,14 +5924,6 @@ path = View; sourceTree = ""; }; - 4BBEE8E12BFEE54100E5E111 /* Resources */ = { - isa = PBXGroup; - children = ( - 4BCF15E92ABB99470083F6DF /* survey-messages.json */, - ); - path = Resources; - sourceTree = ""; - }; 4BCF15E32ABB987F0083F6DF /* NetworkProtection */ = { isa = PBXGroup; children = ( @@ -5994,14 +5961,11 @@ 4BF6961B28BE90E800D402D4 /* HomePage */ = { isa = PBXGroup; children = ( - 4BBEE8E12BFEE54100E5E111 /* Resources */, 56534DEB29DF251C00121467 /* Mocks */, 4BF6961C28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift */, 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */, 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */, 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */, - 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */, - 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */, ); path = HomePage; sourceTree = ""; @@ -9321,7 +9285,6 @@ 3706FE8C293F661700E42796 /* atb-with-update.json in Resources */, 9FBD84622BB3BC6400220859 /* Origin-empty.txt in Resources */, 3706FE8D293F661700E42796 /* DataImportResources in Resources */, - 4BBEE8DF2BFEE07D00E5E111 /* survey-messages.json in Resources */, 3706FE8E293F661700E42796 /* atb.json in Resources */, 9FBD845E2BB3B80300220859 /* Origin.txt in Resources */, 3706FE8F293F661700E42796 /* DuckDuckGo-ExampleCrash.ips in Resources */, @@ -9516,7 +9479,6 @@ B65CD8D52B316FCA00A595BB /* __Snapshots__ in Resources */, B69B50522726CD8100758A2B /* atb.json in Resources */, 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */, - 4BCF15ED2ABB9B180083F6DF /* survey-messages.json in Resources */, B67C6C422654BF49006C872E /* DuckDuckGo-Symbol.jpg in Resources */, B69B50552726CD8100758A2B /* invalid.json in Resources */, 9FBD84612BB3BC6400220859 /* Origin-empty.txt in Resources */, @@ -9972,7 +9934,6 @@ 3706FAD4293F65D500E42796 /* DataExtension.swift in Sources */, 3706FAD6293F65D500E42796 /* ConfigurationStore.swift in Sources */, 3706FAD7293F65D500E42796 /* Feedback.swift in Sources */, - 4B37EE742B4CFF0A00A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */, 1D0DE9422C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */, 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */, 3706FAD9293F65D500E42796 /* FirefoxFaviconsReader.swift in Sources */, @@ -10310,7 +10271,6 @@ 3706FBB6293F65D500E42796 /* ChromiumPreferences.swift in Sources */, 3706FBB7293F65D500E42796 /* FirePopoverViewController.swift in Sources */, 3706FBB8293F65D500E42796 /* SavePaymentMethodPopover.swift in Sources */, - 4BCF15EF2ABBDBFF0083F6DF /* SurveyRemoteMessaging.swift in Sources */, B6CC266D2BAD9CD800F53F8D /* FileProgressPresenter.swift in Sources */, 3706FBB9293F65D500E42796 /* FindInPageViewController.swift in Sources */, 3706FBBA293F65D500E42796 /* Cryptography.swift in Sources */, @@ -10436,7 +10396,6 @@ 3706FC0A293F65D500E42796 /* BookmarkManagedObject.swift in Sources */, B6ABC5972B4861D4008343B9 /* FocusableTextField.swift in Sources */, 3706FC0B293F65D500E42796 /* CSVLoginExporter.swift in Sources */, - 4B37EE722B4CFEE400A89A61 /* SurveyURLBuilder.swift in Sources */, 37197EAC294244D600394917 /* FutureExtension.swift in Sources */, 4B9DB0452A983B24000927DB /* WaitlistModalViewController.swift in Sources */, B66CA41F2AD910B300447CF0 /* DataImportView.swift in Sources */, @@ -10635,7 +10594,6 @@ B6BCC53C2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3706FC8A293F65D500E42796 /* AutoconsentUserScript.swift in Sources */, 3706FC8B293F65D500E42796 /* BookmarksExporter.swift in Sources */, - 4BCF15EE2ABBDBFD0083F6DF /* SurveyRemoteMessage.swift in Sources */, 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */, 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, @@ -10809,7 +10767,6 @@ B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, - 4BBEE8DE2BFEDE3E00E5E111 /* SurveyRemoteMessageTests.swift in Sources */, 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, @@ -10940,7 +10897,6 @@ 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, 3706FE72293F661700E42796 /* ClickToLoadTDSTests.swift in Sources */, 3706FE73293F661700E42796 /* PermissionManagerMock.swift in Sources */, - 4BD57C052AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */, 3706FE74293F661700E42796 /* WebsiteDataStoreMock.swift in Sources */, 3706FE75293F661700E42796 /* WebsiteBreakageReportTests.swift in Sources */, 56D145EF29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, @@ -11577,7 +11533,6 @@ 85B7184E27677CBB00B4277F /* RootView.swift in Sources */, AABEE6AF24AD22B90043105B /* AddressBarTextField.swift in Sources */, B693954C26F04BEB0015B914 /* FocusRingView.swift in Sources */, - 4B37EE632B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */, 4BE41A5E28446EAD00760399 /* BookmarksBarViewModel.swift in Sources */, 4B1E6EF127AB5E5D00F51793 /* NSPopUpButtonView.swift in Sources */, 372A0FEC2B2379310033BF7F /* SyncMetricsEventsHandler.swift in Sources */, @@ -11856,7 +11811,6 @@ 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, - 4BCF15D92ABB8A7F0083F6DF /* SurveyRemoteMessage.swift in Sources */, 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, 4B9DB0292A983B24000927DB /* WaitlistStorage.swift in Sources */, AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */, @@ -11880,7 +11834,6 @@ 85707F31276A7DCA00DC0649 /* OnboardingViewModel.swift in Sources */, 85AC3B0525D6B1D800C7D2AA /* ScriptSourceProviding.swift in Sources */, 4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */, - 4BCF15D72ABB8A110083F6DF /* SurveyRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, F1C70D7C2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, @@ -11946,7 +11899,6 @@ 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 373A1AA8283ED1B900586521 /* BookmarkHTMLReader.swift in Sources */, B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, - 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */, 85378DA0274E6F42007C5CBF /* NSNotificationName+EmailManager.swift in Sources */, 1DDC84FF2B835BC000670238 /* SearchPreferences.swift in Sources */, B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, @@ -12133,7 +12085,6 @@ 3776583127F8325B009A6B35 /* AutofillPreferencesTests.swift in Sources */, 373B2F852C387B830013A94B /* ActiveRemoteMessageModelTests.swift in Sources */, B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, - 4BD57C042AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, @@ -12238,7 +12189,6 @@ AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 4B723E0726B0003E00E14D75 /* CSVImporterTests.swift in Sources */, - 4BCF15EC2ABB9AF80083F6DF /* SurveyRemoteMessageTests.swift in Sources */, B62EB47C25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift in Sources */, 9F0FFFBB2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 4BBC16A527C488C900E00A38 /* DeviceAuthenticatorTests.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index db19fdf2c3..0c4e4ec0aa 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -396,6 +396,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { remoteMessagingClient?.startRefreshingRemoteMessages() + // This messaging system has been replaced by RMF, but we need to clean up the message manifest for any users who had it stored. + let deprecatedRemoteMessagingStorage = DefaultSurveyRemoteMessagingStorage.surveys() + deprecatedRemoteMessagingStorage.removeStoredMessagesIfNecessary() + if didCrashDuringCrashHandlersSetUp { PixelKit.fire(GeneralPixel.crashOnCrashHandlersSetUp) didCrashDuringCrashHandlersSetUp = false diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json deleted file mode 100644 index 85ed0dc199..0000000000 --- a/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Privacy-Pro-128.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf deleted file mode 100644 index 18085b7a09..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf and /dev/null differ diff --git a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift deleted file mode 100644 index 0e32253ec0..0000000000 --- a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// HomePageRemoteMessagingRequest.swift -// -// 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 Networking - -protocol HomePageRemoteMessagingRequest { - - func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error> - -} - -final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingRequest { - - enum SurveysEndpoint { - case debug - case production - - var url: URL { - switch self { - case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/surveys/surveys-debug.json")! - case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/surveys/surveys.json")! - } - } - } - - enum HomePageRemoteMessagingRequestError: Error { - case failedToDecodeMessages - case requestCompletedWithoutErrorOrResponse - } - - static func surveysRequest() -> HomePageRemoteMessagingRequest { -#if DEBUG || REVIEW - return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.debug.url) -#else - return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.production.url) -#endif - } - - private let endpointURL: URL - - init(endpointURL: URL) { - self.endpointURL = endpointURL - } - - func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error> { - let httpMethod = APIRequest.HTTPMethod.get - let configuration = APIRequest.Configuration(url: endpointURL, method: httpMethod, body: nil) - let request = APIRequest(configuration: configuration) - - do { - let response = try await request.fetch() - - guard let data = response.data else { - return .failure(HomePageRemoteMessagingRequestError.requestCompletedWithoutErrorOrResponse) - } - - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode([SurveyRemoteMessage].self, from: data) - return .success(decoded) - } catch { - return .failure(HomePageRemoteMessagingRequestError.failedToDecodeMessages) - } - } catch { - return .failure(error) - } - } - -} diff --git a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift index 7057913dad..9bb7d26b3f 100644 --- a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift +++ b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift @@ -20,9 +20,6 @@ import Foundation protocol SurveyRemoteMessagingStorage { - func store(messages: [SurveyRemoteMessage]) throws - func storedMessages() -> [SurveyRemoteMessage] - func dismissRemoteMessage(with id: String) func dismissedMessageIDs() -> [String] @@ -61,24 +58,6 @@ final class DefaultSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage { self.dismissedMessageIdentifiersKey = dismissedMessageIdentifiersKey } - func store(messages: [SurveyRemoteMessage]) throws { - let encoded = try JSONEncoder().encode(messages) - try encoded.write(to: messagesURL) - } - - func storedMessages() -> [SurveyRemoteMessage] { - do { - let messagesData = try Data(contentsOf: messagesURL) - let messages = try JSONDecoder().decode([SurveyRemoteMessage].self, from: messagesData) - - return messages - } catch { - // Errors can occur if the file doesn't exist or the schema changed, in which case the app will fetch the file again later and - // overwrite it. - return [] - } - } - func dismissRemoteMessage(with id: String) { var dismissedMessages = dismissedMessageIDs() @@ -95,6 +74,10 @@ final class DefaultSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage { return messages ?? [] } + func removeStoredMessagesIfNecessary() { + try? FileManager.default.removeItem(at: messagesURL) + } + func removeStoredAndDismissedMessages() { userDefaults.removeObject(forKey: dismissedMessageIdentifiersKey) try? FileManager.default.removeItem(at: messagesURL) diff --git a/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift deleted file mode 100644 index 50d65f514d..0000000000 --- a/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// SurveyRemoteMessage.swift -// -// 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 Common -import Subscription - -struct SurveyRemoteMessageAction: Codable, Equatable, Hashable { - enum Action: String, Codable { - case openSurveyURL - case openURL - } - - let actionTitle: String - let actionType: Action? - let actionURL: String? -} - -struct SurveyRemoteMessage: Codable, Equatable, Identifiable, Hashable { - - struct Attributes: Codable, Equatable, Hashable { - let subscriptionStatus: String? - let subscriptionBillingPeriod: String? - let sparkleSubscriptionPurchasePlatforms: [String]? - let appStoreSubscriptionPurchasePlatforms: [String]? - let minimumDaysSinceSubscriptionStarted: Int? - let maximumDaysUntilSubscriptionExpirationOrRenewal: Int? - let daysSinceVPNEnabled: Int? - let daysSincePIREnabled: Int? - let hideIfInteractedWithMessage: [String]? - } - - let id: String - let cardTitle: String - let cardDescription: String - let attributes: Attributes - let action: SurveyRemoteMessageAction - - func presentableSurveyURL( - statisticsStore: StatisticsStore = LocalStatisticsStore(), - vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), - operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description, - appVersion: String = AppVersion.shared.versionNumber, - hardwareModel: String? = HardwareModel.model, - subscription: Subscription? - ) -> URL? { - if let actionType = action.actionType, actionType == .openURL, let urlString = action.actionURL, let url = URL(string: urlString) { - return url - } - - guard let actionType = action.actionType, actionType == .openSurveyURL, let surveyURL = action.actionURL else { - return nil - } - - let surveyURLBuilder = SurveyURLBuilder( - statisticsStore: statisticsStore, - operatingSystemVersion: operatingSystemVersion, - appVersion: appVersion, - hardwareModel: hardwareModel, - subscription: subscription, - daysSinceVPNActivated: vpnActivationDateStore.daysSinceActivation(), - daysSinceVPNLastActive: vpnActivationDateStore.daysSinceLastActive(), - daysSincePIRActivated: pirActivationDateStore.daysSinceActivation(), - daysSincePIRLastActive: pirActivationDateStore.daysSinceLastActive() - ) - - return surveyURLBuilder.buildSurveyURL(from: surveyURL) - } -} diff --git a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift deleted file mode 100644 index 19e46880ed..0000000000 --- a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift +++ /dev/null @@ -1,296 +0,0 @@ -// -// SurveyRemoteMessaging.swift -// -// 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 Networking -import PixelKit -import Subscription - -protocol SurveyRemoteMessaging { - - func fetchRemoteMessages() async - func presentableRemoteMessages() -> [SurveyRemoteMessage] - func dismiss(message: SurveyRemoteMessage) - -} - -protocol SurveyRemoteMessageSubscriptionFetching { - - func getSubscription(accessToken: String) async -> Result -} - -final class DefaultSurveyRemoteMessaging: SurveyRemoteMessaging { - - enum Constants { - static let lastRefreshDateKey = "surveys.remote-messaging.last-refresh-date" - } - - private let messageRequest: HomePageRemoteMessagingRequest - private let messageStorage: SurveyRemoteMessagingStorage - private let accountManager: AccountManager - private let subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching - private let vpnActivationDateStore: WaitlistActivationDateStore - private let pirActivationDateStore: WaitlistActivationDateStore - private let minimumRefreshInterval: TimeInterval - private let userDefaults: UserDefaults - - convenience init(subscriptionManager: SubscriptionManager) { - let subscriptionFetcher = SurveyRemoteMessageSubscriptionFetcher(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService) - #if DEBUG || REVIEW - self.init( - accountManager: subscriptionManager.accountManager, - subscriptionFetcher: subscriptionFetcher, - minimumRefreshInterval: .seconds(30) - ) - #else - self.init( - accountManager: subscriptionManager.accountManager, - subscriptionFetcher: subscriptionFetcher, - minimumRefreshInterval: .hours(1) - ) - #endif - } - - init( - messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.surveysRequest(), - messageStorage: SurveyRemoteMessagingStorage = DefaultSurveyRemoteMessagingStorage.surveys(), - accountManager: AccountManager, - subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching, - vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), - minimumRefreshInterval: TimeInterval, - userDefaults: UserDefaults = .standard - ) { - self.messageRequest = messageRequest - self.messageStorage = messageStorage - self.accountManager = accountManager - self.subscriptionFetcher = subscriptionFetcher - self.vpnActivationDateStore = vpnActivationDateStore - self.pirActivationDateStore = pirActivationDateStore - self.minimumRefreshInterval = minimumRefreshInterval - self.userDefaults = userDefaults - } - - func fetchRemoteMessages() async { - if let lastRefreshDate = lastRefreshDate(), lastRefreshDate.addingTimeInterval(minimumRefreshInterval) > Date() { - return - } - - let messageFetchResult = await self.messageRequest.fetchHomePageRemoteMessages() - - switch messageFetchResult { - case .success(let messages): - do { - let processedMessages = await self.process(messages: messages) - try self.messageStorage.store(messages: processedMessages) - self.updateLastRefreshDate() - } catch { - PixelKit.fire(DebugEvent(GeneralPixel.surveyRemoteMessageStorageFailed, error: error)) - } - case .failure(let error): - // Ignore 403 errors, those happen when a file can't be found on S3 - if case APIRequest.Error.invalidStatusCode(403) = error { - self.updateLastRefreshDate() - return - } - - PixelKit.fire(DebugEvent(GeneralPixel.surveyRemoteMessageFetchingFailed, error: error)) - } - } - - // swiftlint:disable cyclomatic_complexity - - /// Processes the messages received from S3 and returns those which the user is eligible for. This is done by checking each of the attributes against the user's local state. - /// Because the result of the message fetch is cached, it means that they won't be immediately updated if the user suddenly qualifies, but the refresh interval for remote messages is only 1 hour so it - /// won't take long for the message to appear to the user. - private func process(messages: [SurveyRemoteMessage]) async -> [SurveyRemoteMessage] { - guard let token = accountManager.accessToken else { - return [] - } - - guard case let .success(subscription) = await subscriptionFetcher.getSubscription(accessToken: token) else { - return [] - } - - return messages.filter { message in - - var attributeMatchStatus = false - - if let interactedMessagesToCheck = message.attributes.hideIfInteractedWithMessage { - let dismissedMessageIDs = messageStorage.dismissedMessageIDs() - for interactedMessage in interactedMessagesToCheck where dismissedMessageIDs.contains(interactedMessage) { - return false - } - } - - // Check subscription status: - if let messageSubscriptionStatus = message.attributes.subscriptionStatus { - if let subscriptionStatus = Subscription.Status(rawValue: messageSubscriptionStatus) { - if subscription.status == subscriptionStatus { - attributeMatchStatus = true - } else { - return false - } - } else { - // If we received a subscription status but can't map it to a valid type, don't show the message. - return false - } - } - - // Check subscription billing period: - if let messageSubscriptionBillingPeriod = message.attributes.subscriptionBillingPeriod { - if let subscriptionBillingPeriod = Subscription.BillingPeriod(rawValue: messageSubscriptionBillingPeriod) { - if subscription.billingPeriod == subscriptionBillingPeriod { - attributeMatchStatus = true - } else { - return false - } - } else { - // If we received a subscription billing period but can't map it to a valid type, don't show the message. - return false - } - } - - // Check subscription start date: - if let messageDaysSinceSubscriptionStarted = message.attributes.minimumDaysSinceSubscriptionStarted { - guard let daysSinceSubscriptionStartDate = Calendar.current.dateComponents( - [.day], from: subscription.startedAt, to: Date() - ).day else { - return false - } - - if daysSinceSubscriptionStartDate >= messageDaysSinceSubscriptionStarted { - attributeMatchStatus = true - } else { - return false - } - } - - // Check subscription end/expiration date: - if let messageDaysUntilSubscriptionExpiration = message.attributes.maximumDaysUntilSubscriptionExpirationOrRenewal { - guard let daysUntilSubscriptionExpiration = Calendar.current.dateComponents( - [.day], from: subscription.expiresOrRenewsAt, to: Date() - ).day else { - return false - } - - if daysUntilSubscriptionExpiration <= messageDaysUntilSubscriptionExpiration { - attributeMatchStatus = true - } else { - return false - } - } - - // Check VPN usage: - if let requiredDaysSinceVPNActivation = message.attributes.daysSinceVPNEnabled { - if let daysSinceActivation = vpnActivationDateStore.daysSinceActivation(), requiredDaysSinceVPNActivation <= daysSinceActivation { - attributeMatchStatus = true - } else { - return false - } - } - - // Check PIR usage: - if let requiredDaysSincePIRActivation = message.attributes.daysSincePIREnabled { - if let daysSinceActivation = pirActivationDateStore.daysSinceActivation(), requiredDaysSincePIRActivation <= daysSinceActivation { - attributeMatchStatus = true - } else { - return false - } - } - - #if APPSTORE - if let appStorePurchasePlatforms = message.attributes.appStoreSubscriptionPurchasePlatforms { - if appStorePurchasePlatforms.contains(subscription.platform.rawValue) { - attributeMatchStatus = true - } else { - return false - } - } - #endif - - #if SPARKLE - if let sparklePurchasePlatforms = message.attributes.sparkleSubscriptionPurchasePlatforms { - if sparklePurchasePlatforms.contains(subscription.platform.rawValue) { - attributeMatchStatus = true - } else { - return false - } - } - #endif - - return attributeMatchStatus - - } - } - - // swiftlint:enable cyclomatic_complexity - - func presentableRemoteMessages() -> [SurveyRemoteMessage] { - let dismissedMessageIDs = messageStorage.dismissedMessageIDs() - let possibleMessages: [SurveyRemoteMessage] = messageStorage.storedMessages() - - let filteredMessages = possibleMessages.filter { message in - if dismissedMessageIDs.contains(message.id) { - return false - } - - return true - - } - - return filteredMessages - } - - func dismiss(message: SurveyRemoteMessage) { - messageStorage.dismissRemoteMessage(with: message.id) - } - - func resetLastRefreshTimestamp() { - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - } - - // MARK: - Private - - private func lastRefreshDate() -> Date? { - guard let object = userDefaults.object(forKey: Constants.lastRefreshDateKey) else { - return nil - } - - guard let date = object as? Date else { - assertionFailure("Got rate limited date, but couldn't convert it to Date") - resetLastRefreshTimestamp() - return nil - } - - return date - } - - private func updateLastRefreshDate() { - userDefaults.setValue(Date(), forKey: Constants.lastRefreshDateKey) - } - -} - -struct SurveyRemoteMessageSubscriptionFetcher: SurveyRemoteMessageSubscriptionFetching { - let subscriptionEndpointService: SubscriptionEndpointService - - func getSubscription(accessToken: String) async -> Result { - return await subscriptionEndpointService.getSubscription(accessToken: accessToken, cachePolicy: .returnCacheDataElseLoad) - } -} diff --git a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift deleted file mode 100644 index 9fa4754f2d..0000000000 --- a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// SurveyURLBuilder.swift -// -// 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 Common -import Subscription - -final class SurveyURLBuilder { - - enum SurveyURLParameters: String, CaseIterable { - case atb = "atb" - case atbVariant = "var" - case macOSVersion = "osv" - case appVersion = "ddgv" - case hardwareModel = "mo" - - case privacyProStatus = "ppro_status" - case privacyProPurchasePlatform = "ppro_platform" - case privacyProBillingPeriod = "ppro_billing" - case privacyProDaysSincePurchase = "ppro_days_since_purchase" - case privacyProDaysUntilExpiration = "ppro_days_until_exp" - - case vpnFirstUsed = "vpn_first_used" - case vpnLastUsed = "vpn_last_used" - case pirFirstUsed = "pir_first_used" - case pirLastUsed = "pir_last_used" - } - - private let statisticsStore: StatisticsStore - private let operatingSystemVersion: String - private let appVersion: String - private let hardwareModel: String? - private let subscription: Subscription? - private let daysSinceVPNActivated: Int? - private let daysSinceVPNLastActive: Int? - private let daysSincePIRActivated: Int? - private let daysSincePIRLastActive: Int? - - init(statisticsStore: StatisticsStore, - operatingSystemVersion: String, - appVersion: String, - hardwareModel: String?, - subscription: Subscription?, - daysSinceVPNActivated: Int?, - daysSinceVPNLastActive: Int?, - daysSincePIRActivated: Int?, - daysSincePIRLastActive: Int?) { - self.statisticsStore = statisticsStore - self.operatingSystemVersion = operatingSystemVersion - self.appVersion = appVersion - self.hardwareModel = hardwareModel - self.subscription = subscription - self.daysSinceVPNActivated = daysSinceVPNActivated - self.daysSinceVPNLastActive = daysSinceVPNLastActive - self.daysSincePIRActivated = daysSincePIRActivated - self.daysSincePIRLastActive = daysSincePIRLastActive - } - - // swiftlint:disable:next cyclomatic_complexity - func buildSurveyURL(from originalURLString: String) -> URL? { - guard var components = URLComponents(string: originalURLString) else { - assertionFailure("Could not build components from survey URL") - return URL(string: originalURLString) - } - - var queryItems = components.queryItems ?? [] - - for parameter in SurveyURLParameters.allCases { - switch parameter { - case .atb: - if let atb = statisticsStore.atb { - queryItems.append(queryItem(parameter: parameter, value: atb)) - } - case .atbVariant: - if let variant = statisticsStore.variant { - queryItems.append(queryItem(parameter: parameter, value: variant)) - } - case .macOSVersion: - queryItems.append(queryItem(parameter: parameter, value: operatingSystemVersion)) - case .appVersion: - queryItems.append(queryItem(parameter: parameter, value: appVersion)) - case .hardwareModel: - if let hardwareModel = hardwareModel { - queryItems.append(queryItem(parameter: parameter, value: hardwareModel)) - } - case .vpnFirstUsed: - if let daysSinceVPNActivated { - queryItems.append(queryItem(parameter: parameter, value: daysSinceVPNActivated)) - } - case .vpnLastUsed: - if let daysSinceVPNLastActive { - queryItems.append(queryItem(parameter: parameter, value: daysSinceVPNLastActive)) - } - case .pirFirstUsed: - if let daysSincePIRActivated { - queryItems.append(queryItem(parameter: parameter, value: daysSincePIRActivated)) - } - case .pirLastUsed: - if let daysSincePIRLastActive { - queryItems.append(queryItem(parameter: parameter, value: daysSincePIRLastActive)) - } - case .privacyProStatus: - if let privacyProStatus = subscription?.status { - switch privacyProStatus { - case .autoRenewable: queryItems.append(queryItem(parameter: parameter, value: "auto_renewable")) - case .notAutoRenewable: queryItems.append(queryItem(parameter: parameter, value: "not_auto_renewable")) - case .gracePeriod: queryItems.append(queryItem(parameter: parameter, value: "grace_period")) - case .inactive: queryItems.append(queryItem(parameter: parameter, value: "inactive")) - case .expired: queryItems.append(queryItem(parameter: parameter, value: "expired")) - case .unknown: queryItems.append(queryItem(parameter: parameter, value: "unknown")) - } - } - case .privacyProPurchasePlatform: - if let privacyProPurchasePlatform = subscription?.platform { - switch privacyProPurchasePlatform { - case .apple: queryItems.append(queryItem(parameter: parameter, value: "apple")) - case .google: queryItems.append(queryItem(parameter: parameter, value: "google")) - case .stripe: queryItems.append(queryItem(parameter: parameter, value: "stripe")) - case .unknown: queryItems.append(queryItem(parameter: parameter, value: "unknown")) - } - } - case .privacyProBillingPeriod: - if let privacyProBillingPeriod = subscription?.billingPeriod { - switch privacyProBillingPeriod { - case .monthly: queryItems.append(queryItem(parameter: parameter, value: "monthly")) - case .yearly: queryItems.append(queryItem(parameter: parameter, value: "yearly")) - case .unknown: queryItems.append(queryItem(parameter: parameter, value: "unknown")) - } - } - case .privacyProDaysSincePurchase: - if let startedAt = subscription?.startedAt, let daysSincePurchase = daysSince(date: startedAt) { - queryItems.append(queryItem(parameter: parameter, value: daysSincePurchase)) - } - case .privacyProDaysUntilExpiration: - if let expiresOrRenewsAt = subscription?.expiresOrRenewsAt, let daysUntilExpiry = daysSince(date: expiresOrRenewsAt) { - queryItems.append(queryItem(parameter: parameter, value: daysUntilExpiry)) - } - } - } - - components.queryItems = queryItems - - return components.url - } - - private func queryItem(parameter: SurveyURLParameters, value: String) -> URLQueryItem { - let urlAllowed: CharacterSet = .alphanumerics.union(.init(charactersIn: "-._~")) - let sanitizedValue = value.addingPercentEncoding(withAllowedCharacters: urlAllowed) - return URLQueryItem(name: parameter.rawValue, value: sanitizedValue) - } - - private func queryItem(parameter: SurveyURLParameters, value: Int) -> URLQueryItem { - return URLQueryItem(name: parameter.rawValue, value: String(describing: value)) - } - - private func daysSince(date storedDate: Date) -> Int? { - if let days = Calendar.current.dateComponents([.day], from: storedDate, to: Date()).day { - return abs(days) - } - - return nil - } - -} diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index b9a8f363b8..ed30fc31f3 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -40,7 +40,6 @@ extension HomePage.Models { let gridWidth = FeaturesGridDimensions.width let deleteActionTitle = UserText.newTabSetUpRemoveItemAction let privacyConfigurationManager: PrivacyConfigurationManaging - let surveyRemoteMessaging: SurveyRemoteMessaging let permanentSurveyManager: SurveyManager var duckPlayerURL: String { @@ -108,7 +107,6 @@ extension HomePage.Models { tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), duckPlayerPreferences: DuckPlayerPreferencesPersistor, - surveyRemoteMessaging: SurveyRemoteMessaging, privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, permanentSurveyManager: SurveyManager = PermanentSurveyManager(), subscriptionManager: SubscriptionManager = Application.appDelegate.subscriptionManager) { @@ -118,7 +116,6 @@ extension HomePage.Models { self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager self.duckPlayerPreferences = duckPlayerPreferences - self.surveyRemoteMessaging = surveyRemoteMessaging self.privacyConfigurationManager = privacyConfigurationManager self.permanentSurveyManager = permanentSurveyManager self.subscriptionManager = subscriptionManager @@ -143,8 +140,6 @@ extension HomePage.Models { performEmailProtectionAction() case .permanentSurvey: visitSurvey() - case .surveyRemoteMessage(let message): - handle(remoteMessage: message) } } @@ -195,23 +190,13 @@ extension HomePage.Models { shouldShowEmailProtectionSetting = false case .permanentSurvey: shouldShowPermanentSurvey = false - case .surveyRemoteMessage(let message): - surveyRemoteMessaging.dismiss(message: message) - PixelKit.fire(GeneralPixel.surveyRemoteMessageDismissed(messageID: message.id)) } refreshFeaturesMatrix() } func refreshFeaturesMatrix() { var features: [FeatureType] = [] - - for message in surveyRemoteMessaging.presentableRemoteMessages() { - features.append(.surveyRemoteMessage(message)) - PixelKit.fire(GeneralPixel.surveyRemoteMessageDisplayed(messageID: message.id), frequency: .daily) - } - appendFeatureCards(&features) - featuresMatrix = features.chunked(into: itemsPerRow) } @@ -235,8 +220,6 @@ extension HomePage.Models { return shouldEmailProtectionCardBeVisible case .permanentSurvey: return shouldPermanentSurveyBeVisible - case .surveyRemoteMessage: - return false // This is handled separately } } @@ -312,8 +295,7 @@ extension HomePage.Models { private var shouldPermanentSurveyBeVisible: Bool { return shouldShowPermanentSurvey && - permanentSurveyManager.isSurveyAvailable && - surveyRemoteMessaging.presentableRemoteMessages().isEmpty // When Privacy Pro survey is visible, ensure we do not show multiple at once + permanentSurveyManager.isSurveyAvailable } @MainActor private func visitSurvey() { @@ -324,44 +306,6 @@ extension HomePage.Models { shouldShowPermanentSurvey = false } - @MainActor private func handle(remoteMessage: SurveyRemoteMessage) { - guard let actionType = remoteMessage.action.actionType else { - PixelKit.fire(GeneralPixel.surveyRemoteMessageDismissed(messageID: remoteMessage.id)) - surveyRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() - return - } - - switch actionType { - case .openSurveyURL, .openURL: - Task { @MainActor in - var subscription: Subscription? - - if let token = subscriptionManager.accountManager.accessToken { - switch await subscriptionManager.subscriptionEndpointService.getSubscription( - accessToken: token, - cachePolicy: .returnCacheDataElseLoad - ) { - case .success(let fetchedSubscription): - subscription = fetchedSubscription - case .failure: - break - } - } - - if let surveyURL = remoteMessage.presentableSurveyURL(subscription: subscription) { - let tab = Tab(content: .url(surveyURL, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - PixelKit.fire(GeneralPixel.surveyRemoteMessageOpened(messageID: remoteMessage.id)) - - // Dismiss the message after the user opens the URL, even if they just close the tab immediately afterwards. - surveyRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() - } - } - } - } - } // MARK: Feature Type @@ -384,7 +328,6 @@ extension HomePage.Models { case dock case importBookmarksAndPasswords case permanentSurvey - case surveyRemoteMessage(SurveyRemoteMessage) var title: String { switch self { @@ -400,8 +343,6 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionCardTitle case .permanentSurvey: return PermanentSurveyManager.title - case .surveyRemoteMessage(let message): - return message.cardTitle } } @@ -419,8 +360,6 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionSummary case .permanentSurvey: return PermanentSurveyManager.body - case .surveyRemoteMessage(let message): - return message.cardDescription } } @@ -438,8 +377,6 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionAction case .permanentSurvey: return PermanentSurveyManager.actionTitle - case .surveyRemoteMessage(let message): - return message.action.actionTitle } } @@ -468,8 +405,6 @@ extension HomePage.Models { return .inbox128.resized(to: iconSize)! case .permanentSurvey: return .survey128.resized(to: iconSize)! - case .surveyRemoteMessage: - return .privacyProSurvey.resized(to: iconSize)! } } } diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index a5be886754..74eb43f964 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -155,10 +155,7 @@ final class HomePageViewController: NSViewController { dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, - duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), - surveyRemoteMessaging: DefaultSurveyRemoteMessaging( - subscriptionManager: Application.appDelegate.subscriptionManager - ) + duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor() ) } diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 0c2f203963..56d49002c3 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -198,7 +198,6 @@ final class MainViewController: NSViewController { updateReloadMenuItem() updateStopMenuItem() browserTabViewController.windowDidBecomeKey() - refreshSurveyMessages() } func windowDidResignKey() { @@ -220,16 +219,6 @@ final class MainViewController: NSViewController { } } - private lazy var surveyMessaging: DefaultSurveyRemoteMessaging = { - return DefaultSurveyRemoteMessaging(subscriptionManager: Application.appDelegate.subscriptionManager) - }() - - func refreshSurveyMessages() { - Task { - await surveyMessaging.fetchRemoteMessages() - } - } - override func encodeRestorableState(with coder: NSCoder) { fatalError("Default AppKit State Restoration should not be used") } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 0c2a87cb41..ef5b69c09c 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -658,10 +658,6 @@ final class MainMenu: NSMenu { openSubscriptionTab: { WindowControllersManager.shared.showTab(with: .subscription($0)) }, subscriptionManager: Application.appDelegate.subscriptionManager) - NSMenuItem(title: "Privacy Pro Survey") { - NSMenuItem(title: "Reset Remote Message Cache", action: #selector(MainViewController.resetSurveyRemoteMessages)) - } - NSMenuItem(title: "Logging").submenu(setupLoggingMenu()) } debugMenu.addItem(internalUserItem) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 4d02d356fd..11ad9ce9af 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -969,11 +969,6 @@ extension MainViewController { setConfigurationUrl(nil) } - @objc func resetSurveyRemoteMessages(_ sender: Any?) { - DefaultSurveyRemoteMessagingStorage.surveys().removeStoredAndDismissedMessages() - DefaultSurveyRemoteMessaging(subscriptionManager: Application.appDelegate.subscriptionManager).resetLastRefreshTimestamp() - } - // MARK: - Developer Tools @objc func toggleDeveloperTools(_ sender: Any?) { diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index f23bb61dea..01774dd19a 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -135,11 +135,6 @@ enum GeneralPixel: PixelKitEventV2 { case dashboardProtectionAllowlistAdd(triggerOrigin: String?) case dashboardProtectionAllowlistRemove(triggerOrigin: String?) - // Survey - case surveyRemoteMessageDisplayed(messageID: String) - case surveyRemoteMessageDismissed(messageID: String) - case surveyRemoteMessageOpened(messageID: String) - // VPN case vpnBreakageReport(category: String, description: String, metadata: String) @@ -404,8 +399,6 @@ enum GeneralPixel: PixelKitEventV2 { case burnerTabMisplaced - case surveyRemoteMessageFetchingFailed - case surveyRemoteMessageStorageFailed case loginItemUpdateError(loginItemBundleID: String, action: String, buildType: String, osVersion: String) // Tracks installation without tracking retention. @@ -626,12 +619,6 @@ enum GeneralPixel: PixelKitEventV2 { case .vpnBreakageReport: return "m_mac_vpn_breakage_report" - case .surveyRemoteMessageDisplayed(let messageID): - return "m_mac_survey_remote_message_displayed_\(messageID)" - case .surveyRemoteMessageDismissed(let messageID): - return "m_mac_survey_remote_message_dismissed_\(messageID)" - case .surveyRemoteMessageOpened(let messageID): - return "m_mac_survey_remote_message_opened_\(messageID)" case .networkProtectionEnabledOnSearch: return "m_mac_netp_ev_enabled_on_search" @@ -995,8 +982,6 @@ enum GeneralPixel: PixelKitEventV2 { case .burnerTabMisplaced: return "burner_tab_misplaced" - case .surveyRemoteMessageFetchingFailed: return "survey_remote_message_fetching_failed" - case .surveyRemoteMessageStorageFailed: return "survey_remote_message_storage_failed" case .loginItemUpdateError: return "login-item_update-error" // Installation Attribution diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 09467d8abe..24f4d1c6ec 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -21,22 +21,6 @@ import BrowserServicesKit import Common @testable import DuckDuckGo_Privacy_Browser -final class MockSurveyRemoteMessaging: SurveyRemoteMessaging { - - var messages: [SurveyRemoteMessage] = [] - - func fetchRemoteMessages() async { - return - } - - func presentableRemoteMessages() -> [SurveyRemoteMessage] { - messages - } - - func dismiss(message: SurveyRemoteMessage) {} - -} - final class ContinueSetUpModelTests: XCTestCase { var vm: HomePage.Models.ContinueSetUpModel! @@ -50,7 +34,6 @@ final class ContinueSetUpModelTests: XCTestCase { var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! var dockCustomizer: DockCustomization! - var mockSurveyMessaging: SurveyRemoteMessaging! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @MainActor override func setUp() { @@ -68,7 +51,6 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() - mockSurveyMessaging = MockSurveyRemoteMessaging() dockCustomizer = DockCustomizerMock() vm = HomePage.Models.ContinueSetUpModel( @@ -78,7 +60,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: mockSurveyMessaging, privacyConfigurationManager: privacyConfigManager, permanentSurveyManager: MockPermanentSurveyManager() ) @@ -122,7 +103,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -140,7 +120,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -348,7 +327,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -365,7 +343,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) vm.shouldShowAllFeatures = true @@ -470,7 +447,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: createMessaging(), privacyConfigurationManager: privacyConfigManager, permanentSurveyManager: surveyManager ) @@ -501,10 +477,6 @@ final class ContinueSetUpModelTests: XCTestCase { return features.chunked(into: HomePage.featuresPerRow) } - private func createMessaging() -> SurveyRemoteMessaging { - MockSurveyRemoteMessaging() - } - @MainActor func test_WhenUserDoesntHaveApplicationInTheDock_ThenAddToDockCardIsDisplayed() { #if !APPSTORE let dockCustomizer = DockCustomizerMock() @@ -553,7 +525,6 @@ extension HomePage.Models.ContinueSetUpModel { tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - surveyRemoteMessaging: MockSurveyRemoteMessaging(), privacyConfigurationManager: manager, permanentSurveyManager: permanentSurveyManager) } diff --git a/UnitTests/HomePage/Resources/survey-messages.json b/UnitTests/HomePage/Resources/survey-messages.json deleted file mode 100644 index 5e068db349..0000000000 --- a/UnitTests/HomePage/Resources/survey-messages.json +++ /dev/null @@ -1,43 +0,0 @@ -[ - { - "id": "message-1", - "cardTitle": "Title 1", - "cardDescription": "Description 1", - "attributes": { - "subscriptionStatus": "Auto-Renewable", - "subscriptionBillingPeriod": "Monthly", - "minimumDaysSinceSubscriptionStarted": 1, - "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, - "daysSinceVPNEnabled": 2, - "daysSincePIREnabled": 3, - "sparkleSubscriptionPurchasePlatforms": ["stripe"], - "appStoreSubscriptionPurchasePlatforms": ["stripe"] - }, - "action": { - "actionTitle": "Action 1", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - }, - { - "id": "message-2", - "cardTitle": "Title 2", - "cardDescription": "Description 2", - "attributes": { - "subscriptionStatus": "Auto-Renewable", - "subscriptionBillingPeriod": "Monthly", - "minimumDaysSinceSubscriptionStarted": 1, - "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, - "daysSinceVPNEnabled": 2, - "daysSincePIREnabled": 3, - "sparkleSubscriptionPurchasePlatforms": ["stripe"], - "appStoreSubscriptionPurchasePlatforms": ["stripe"], - "hideIfInteractedWithMessage": ["message-1"] - }, - "action": { - "actionTitle": "Action 1", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - } -] diff --git a/UnitTests/HomePage/SurveyRemoteMessageTests.swift b/UnitTests/HomePage/SurveyRemoteMessageTests.swift deleted file mode 100644 index 2adc1623e5..0000000000 --- a/UnitTests/HomePage/SurveyRemoteMessageTests.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// SurveyRemoteMessageTests.swift -// -// 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 XCTest -@testable import DuckDuckGo_Privacy_Browser - -final class SurveyRemoteMessageTests: XCTestCase { - - func testWhenDecodingMessages_ThenMessagesDecodeSuccessfully() throws { - let mockStatisticsStore = MockStatisticsStore() - mockStatisticsStore.atb = "atb-123" - mockStatisticsStore.variant = "variant" - - let mockActivationDateStore = MockWaitlistActivationDateStore() - mockActivationDateStore._daysSinceActivation = 0 - mockActivationDateStore._daysSinceLastActive = 0 - - let fileURL = mockMessagesURL() - let data = try Data(contentsOf: fileURL) - - let decoder = JSONDecoder() - let decodedMessages = try decoder.decode([SurveyRemoteMessage].self, from: data) - - XCTAssertEqual(decodedMessages.count, 2) - - guard let firstMessage = decodedMessages.first(where: { $0.id == "message-1"}) else { - XCTFail("Failed to find expected message") - return - } - - let firstMessagePresentableSurveyURL = firstMessage.presentableSurveyURL( - statisticsStore: mockStatisticsStore, - vpnActivationDateStore: mockActivationDateStore, - operatingSystemVersion: "1.2.3", - appVersion: "4.5.6", - hardwareModel: "MacBookPro,123", - subscription: nil - ) - - XCTAssertEqual(firstMessage.cardTitle, "Title 1") - XCTAssertEqual(firstMessage.cardDescription, "Description 1") - XCTAssertEqual(firstMessage.action.actionTitle, "Action 1") - XCTAssertEqual(firstMessage.attributes.minimumDaysSinceSubscriptionStarted, 1) - XCTAssertEqual(firstMessage.attributes.daysSinceVPNEnabled, 2) - XCTAssertEqual(firstMessage.attributes.daysSincePIREnabled, 3) - XCTAssertEqual(firstMessage.attributes.maximumDaysUntilSubscriptionExpirationOrRenewal, 30) - XCTAssertEqual(firstMessage.attributes.appStoreSubscriptionPurchasePlatforms, ["stripe"]) - XCTAssertEqual(firstMessage.attributes.sparkleSubscriptionPurchasePlatforms, ["stripe"]) - XCTAssertNotNil(firstMessagePresentableSurveyURL) - - guard let lastMessage = decodedMessages.first(where: { $0.id == "message-2"}) else { - XCTFail("Failed to find expected message") - return - } - - XCTAssertEqual(lastMessage.attributes.hideIfInteractedWithMessage, ["message-1"]) - } - - func testWhenGettingSurveyURL_AndSurveyURLHasParameters_ThenParametersAreReplaced() { - let remoteMessageJSON = """ - { - "id": "1", - "daysSinceNetworkProtectionEnabled": 0, - "cardTitle": "Title", - "cardDescription": "Description", - "attributes": { - "subscriptionStatus": "", - "minimumDaysSinceSubscriptionStarted": 1, - "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, - "daysSinceVPNEnabled": 1, - "daysSincePIREnabled": 1 - }, - "action": { - "actionTitle": "Action", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - } - """ - - let decoder = JSONDecoder() - let message: SurveyRemoteMessage - do { - message = try decoder.decode(SurveyRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) - } catch { - XCTFail("Failed to decode with error: \(error.localizedDescription)") - return - } - - let mockStatisticsStore = MockStatisticsStore() - mockStatisticsStore.atb = "atb-123" - mockStatisticsStore.variant = "variant" - - let mockActivationDateStore = MockWaitlistActivationDateStore() - mockActivationDateStore._daysSinceActivation = 2 - mockActivationDateStore._daysSinceLastActive = 1 - - let presentableSurveyURL = message.presentableSurveyURL( - statisticsStore: mockStatisticsStore, - vpnActivationDateStore: mockActivationDateStore, - operatingSystemVersion: "1.2.3", - appVersion: "4.5.6", - hardwareModel: "MacBookPro,123", - subscription: nil - ) - - let expectedURL = """ - https://duckduckgo.com/?atb=atb-123&var=variant&osv=1.2.3&ddgv=4.5.6&mo=MacBookPro%252C123&vpn_first_used=2&vpn_last_used=1 - """ - - XCTAssertEqual(presentableSurveyURL!.absoluteString, expectedURL) - } - - private func mockMessagesURL() -> URL { - let bundle = Bundle(for: SurveyRemoteMessageTests.self) - return bundle.resourceURL!.appendingPathComponent("survey-messages.json") - } - -} diff --git a/UnitTests/HomePage/SurveyRemoteMessagingTests.swift b/UnitTests/HomePage/SurveyRemoteMessagingTests.swift deleted file mode 100644 index 9481684bf5..0000000000 --- a/UnitTests/HomePage/SurveyRemoteMessagingTests.swift +++ /dev/null @@ -1,399 +0,0 @@ -// -// SurveyRemoteMessagingTests.swift -// -// 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 XCTest -import SubscriptionTestingUtilities -@testable import Subscription -@testable import DuckDuckGo_Privacy_Browser - -@available(macOS 12.0, *) -final class SurveyRemoteMessagingTests: XCTestCase { - - private var defaults: UserDefaults! - private let testGroupName = "remote-messaging" - - private var accountManager: AccountManager! - private var subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching! - - override func setUp() { - defaults = UserDefaults(suiteName: testGroupName)! - defaults.removePersistentDomain(forName: testGroupName) - - accountManager = AccountManagerMock(accessToken: "mock-token") - subscriptionFetcher = MockSubscriptionFetcher() - } - - func testWhenFetchingRemoteMessages_AndTheUserDidNotSignUpViaWaitlist_ThenMessagesAreFetched() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - request.result = .success([]) - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - await messaging.fetchRemoteMessages() - - XCTAssertTrue(request.didFetchMessages) - } - - func testWhenFetchingRemoteMessages_AndTheUserDidSignUpViaWaitlist_ButUserHasNotActivatedNetP_ThenMessagesAreFetched() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - request.result = .success([]) - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertNil(activationDateStorage.daysSinceActivation()) - - await messaging.fetchRemoteMessages() - - XCTAssertTrue(request.didFetchMessages) - } - - func testWhenFetchingRemoteMessages_AndPurchasePlatformDoesNotMatch_ThenMessagesAreNotStored() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - let messages = [mockMessage(id: "123", purchasePlatform: "stripe")] - - request.result = .success(messages) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertEqual(storage.storedMessages(), []) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - await messaging.fetchRemoteMessages() - - XCTAssertTrue(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), []) - } - - func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ThenMessagesAreFetched_AndMessagesAreStored() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - let messages = [mockMessage(id: "123")] - - request.result = .success(messages) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertEqual(storage.storedMessages(), []) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - await messaging.fetchRemoteMessages() - - XCTAssertTrue(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), messages) - } - - func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ButRateLimitedOperationCannotRunAgain_ThenMessagesAreNotFetched() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - activationDateStorage._daysSinceActivation = 10 - - defaults.setValue(Date(), forKey: DefaultSurveyRemoteMessaging.Constants.lastRefreshDateKey) - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: .days(7), // Use a large number to hit the refresh check - userDefaults: defaults - ) - - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - await messaging.fetchRemoteMessages() - - XCTAssertFalse(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), []) - } - - func testWhenStoredMessagesExist_AndSomeMessagesHaveBeenDismissed_ThenPresentableMessagesDoNotIncludeDismissedMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - let dismissedMessage = mockMessage(id: "123") - let activeMessage = mockMessage(id: "456") - try? storage.store(messages: [dismissedMessage, activeMessage]) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessagesBefore = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessagesBefore, [dismissedMessage, activeMessage]) - messaging.dismiss(message: dismissedMessage) - let presentableMessagesAfter = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessagesAfter, [activeMessage]) - } - - func testWhenStoredMessagesExist_AndSomeMessagesRequireNetPUsage_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - - let message = mockMessage(id: "123") - try? storage.store(messages: [message]) - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessages = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessages, [message]) - } - - func testWhenMessageHasBeenDismissed_AndOtherMessageShouldHideBasedOnThat_ThenPresentableMessagesAreEmpty() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let dismissedMessageID = "dismissed-message" - - let messages = [ - mockMessage(id: "123", hideIfInteractedWithMessage: dismissedMessageID) - ] - - request.result = .success(messages) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - storage.dismissRemoteMessage(with: dismissedMessageID) - XCTAssertEqual(storage.storedMessages(), []) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - await messaging.fetchRemoteMessages() - - XCTAssertTrue(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), []) - } - - func testWhenMessageHasBeenDismissed_AndOtherMessageShouldNotHideBasedOnThat_ThenPresentableMessagesAreNotEmpty() async { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockSurveyRemoteMessagingStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let dismissedMessageID = "dismissed-message" - - let messages = [ - mockMessage(id: "123", hideIfInteractedWithMessage: dismissedMessageID) - ] - - request.result = .success(messages) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultSurveyRemoteMessaging( - messageRequest: request, - messageStorage: storage, - accountManager: accountManager, - subscriptionFetcher: subscriptionFetcher, - vpnActivationDateStore: activationDateStorage, - pirActivationDateStore: activationDateStorage, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - storage.dismissRemoteMessage(with: "unrelated-dismissed-message") - XCTAssertEqual(storage.storedMessages(), []) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - await messaging.fetchRemoteMessages() - - XCTAssertTrue(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), messages) - } - - private func mockMessage(id: String, - subscriptionStatus: String = Subscription.Status.autoRenewable.rawValue, - minimumDaysSinceSubscriptionStarted: Int = 0, - maximumDaysUntilSubscriptionExpirationOrRenewal: Int = 0, - daysSinceVPNEnabled: Int = 0, - daysSincePIREnabled: Int = 0, - purchasePlatform: String = "apple", - hideIfInteractedWithMessage: String = "") -> SurveyRemoteMessage { - let remoteMessageJSON = """ - { - "id": "\(id)", - "cardTitle": "Title", - "cardDescription": "Description 1", - "attributes": { - "subscriptionStatus": "\(subscriptionStatus)", - "minimumDaysSinceSubscriptionStarted": \(minimumDaysSinceSubscriptionStarted), - "maximumDaysUntilSubscriptionExpirationOrRenewal": \(maximumDaysUntilSubscriptionExpirationOrRenewal), - "daysSinceVPNEnabled": \(daysSinceVPNEnabled), - "daysSincePIREnabled": \(daysSincePIREnabled), - "sparkleSubscriptionPurchasePlatforms": ["\(purchasePlatform)"], - "appStoreSubscriptionPurchasePlatforms": ["\(purchasePlatform)"], - "hideIfInteractedWithMessage": ["\(hideIfInteractedWithMessage)"] - }, - "action": { - "actionTitle": "Action 1" - } - } - """ - - let decoder = JSONDecoder() - return try! decoder.decode(SurveyRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) - } - -} - -// MARK: - Mocks - -private final class MockNetworkProtectionRemoteMessagingRequest: HomePageRemoteMessagingRequest { - - var result: Result<[SurveyRemoteMessage], Error>! - var didFetchMessages: Bool = false - - func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], any Error> { - didFetchMessages = true - return result - } - -} - -private final class MockSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage { - - var _storedMessages: [SurveyRemoteMessage] = [] - var _storedDismissedMessageIDs: [String] = [] - - func store(messages: [SurveyRemoteMessage]) throws { - self._storedMessages = messages - } - - func storedMessages() -> [SurveyRemoteMessage] { - _storedMessages - } - - func dismissRemoteMessage(with id: String) { - if !_storedDismissedMessageIDs.contains(id) { - _storedDismissedMessageIDs.append(id) - } - } - - func dismissedMessageIDs() -> [String] { - _storedDismissedMessageIDs - } - -} - -final class MockWaitlistActivationDateStore: WaitlistActivationDateStore { - - var _daysSinceActivation: Int? - var _daysSinceLastActive: Int? - - func daysSinceActivation() -> Int? { - _daysSinceActivation - } - - func daysSinceLastActive() -> Int? { - _daysSinceLastActive - } - -} - -final class MockSubscriptionFetcher: SurveyRemoteMessageSubscriptionFetching { - - var subscription: Subscription = Subscription( - productId: "product", - name: "name", - billingPeriod: .monthly, - startedAt: Date(), - expiresOrRenewsAt: Date(), - platform: .apple, - status: .autoRenewable) - - func getSubscription(accessToken: String) async -> Result { - return .success(subscription) - } - -}