diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 02e94cdf9a..c18322e0a6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1129,14 +1129,9 @@ 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 */; }; - 4B37EE6F2B4CFE8500A89A61 /* dbp-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B37EE6E2B4CFE8500A89A61 /* dbp-messages.json */; }; 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 */; }; - 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */; }; - 4B37EE762B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */; }; - 4B37EE772B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */; }; - 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.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 */; }; @@ -1353,6 +1348,8 @@ 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 */; }; @@ -1362,16 +1359,16 @@ 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 /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; - 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; - 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */; }; - 4BCF15ED2ABB9B180083F6DF /* network-protection-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BCF15E92ABB99470083F6DF /* network-protection-messages.json */; }; - 4BCF15EE2ABBDBFD0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; - 4BCF15EF2ABBDBFF0083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; + 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 /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; - 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; + 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 */; }; @@ -3120,9 +3117,6 @@ 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 = ""; }; - 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionRemoteMessage.swift; sourceTree = ""; }; - 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionRemoteMessaging.swift; sourceTree = ""; }; - 4B37EE6E2B4CFE8500A89A61 /* dbp-messages.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dbp-messages.json"; 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 = ""; }; @@ -3300,13 +3294,13 @@ 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 /* NetworkProtectionRemoteMessaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessaging.swift; sourceTree = ""; }; - 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessage.swift; sourceTree = ""; }; - 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessageTests.swift; sourceTree = ""; }; - 4BCF15E92ABB99470083F6DF /* network-protection-messages.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "network-protection-messages.json"; 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 /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; 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 = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; @@ -4636,7 +4630,6 @@ 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3169132B2BD2C7960051B46D /* ErrorView */, 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */, - 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */, ); path = DBP; sourceTree = ""; @@ -5064,22 +5057,15 @@ 4B37EE5B2B4CFC3C00A89A61 /* Surveys */ = { isa = PBXGroup; children = ( - 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */, - 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */, 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */, + 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */, + 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */, + 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */, + 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */, ); path = Surveys; sourceTree = ""; }; - 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */ = { - isa = PBXGroup; - children = ( - 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */, - 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */, - ); - path = RemoteMessaging; - sourceTree = ""; - }; 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */ = { isa = PBXGroup; children = ( @@ -5172,7 +5158,6 @@ 4B4D60612A0B29FA00BCD287 /* DeveloperIDTarget */ = { isa = PBXGroup; children = ( - 4BCF15D52ABB83D70083F6DF /* NetworkProtectionRemoteMessaging */, 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */, 4B2F565B2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift */, ); @@ -5732,22 +5717,18 @@ path = View; sourceTree = ""; }; - 4BCF15D52ABB83D70083F6DF /* NetworkProtectionRemoteMessaging */ = { + 4BBEE8E12BFEE54100E5E111 /* Resources */ = { isa = PBXGroup; children = ( - 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */, - 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */, + 4BCF15E92ABB99470083F6DF /* survey-messages.json */, ); - path = NetworkProtectionRemoteMessaging; + path = Resources; sourceTree = ""; }; 4BCF15E32ABB987F0083F6DF /* NetworkProtection */ = { isa = PBXGroup; children = ( BDA7648F2BC4E56200D0400C /* Mocks */, - 4BCF15E62ABB98A20083F6DF /* Resources */, - 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, - 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */, @@ -5755,15 +5736,6 @@ path = NetworkProtection; sourceTree = ""; }; - 4BCF15E62ABB98A20083F6DF /* Resources */ = { - isa = PBXGroup; - children = ( - 4B37EE6E2B4CFE8500A89A61 /* dbp-messages.json */, - 4BCF15E92ABB99470083F6DF /* network-protection-messages.json */, - ); - path = Resources; - sourceTree = ""; - }; 4BD18F02283F0F1000058124 /* View */ = { isa = PBXGroup; children = ( @@ -5790,11 +5762,14 @@ 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 = ""; @@ -9033,6 +9008,7 @@ 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 */, @@ -9226,10 +9202,9 @@ B69B50542726CD8100758A2B /* atb-with-update.json in Resources */, 37A803DB27FD69D300052F4C /* DataImportResources in Resources */, B65CD8D52B316FCA00A595BB /* __Snapshots__ in Resources */, - 4B37EE6F2B4CFE8500A89A61 /* dbp-messages.json in Resources */, B69B50522726CD8100758A2B /* atb.json in Resources */, 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */, - 4BCF15ED2ABB9B180083F6DF /* network-protection-messages.json 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 */, @@ -9910,7 +9885,6 @@ 3706FB7E293F65D500E42796 /* FireCoordinator.swift in Sources */, 3706FB7F293F65D500E42796 /* GeolocationProvider.swift in Sources */, B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */, - 4B37EE762B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, 3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, B677FC552B064A9C0099EB04 /* DataImportViewModel.swift in Sources */, D64A5FF92AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, @@ -10007,7 +9981,7 @@ 3706FBB6293F65D500E42796 /* ChromiumPreferences.swift in Sources */, 3706FBB7293F65D500E42796 /* FirePopoverViewController.swift in Sources */, 3706FBB8293F65D500E42796 /* SavePaymentMethodPopover.swift in Sources */, - 4BCF15EF2ABBDBFF0083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, + 4BCF15EF2ABBDBFF0083F6DF /* SurveyRemoteMessaging.swift in Sources */, B6CC266D2BAD9CD800F53F8D /* FileProgressPresenter.swift in Sources */, 3706FBB9293F65D500E42796 /* FindInPageViewController.swift in Sources */, 3706FBBA293F65D500E42796 /* Cryptography.swift in Sources */, @@ -10156,7 +10130,6 @@ 1DC669712B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, 3706FC19293F65D500E42796 /* NSNotificationName+Favicons.swift in Sources */, 3706FC1A293F65D500E42796 /* PinningManager.swift in Sources */, - 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, 3706FC1B293F65D500E42796 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */, 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, @@ -10319,7 +10292,7 @@ B6BCC53C2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3706FC8A293F65D500E42796 /* AutoconsentUserScript.swift in Sources */, 3706FC8B293F65D500E42796 /* BookmarksExporter.swift in Sources */, - 4BCF15EE2ABBDBFD0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, + 4BCF15EE2ABBDBFD0083F6DF /* SurveyRemoteMessage.swift in Sources */, 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */, 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, @@ -10479,6 +10452,7 @@ 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 */, @@ -10598,7 +10572,7 @@ 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, 3706FE72293F661700E42796 /* ClickToLoadTDSTests.swift in Sources */, 3706FE73293F661700E42796 /* PermissionManagerMock.swift in Sources */, - 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, + 4BD57C052AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */, 3706FE74293F661700E42796 /* WebsiteDataStoreMock.swift in Sources */, 3706FE75293F661700E42796 /* WebsiteBreakageReportTests.swift in Sources */, 56D145EF29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, @@ -11222,7 +11196,6 @@ 4BBC16A227C485BC00E00A38 /* DeviceIdleStateDetector.swift in Sources */, 4B379C2427BDE1B0008A968E /* FlatButton.swift in Sources */, 37054FC92873301700033B6F /* PinnedTabView.swift in Sources */, - 4B37EE772B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, 4BA1A6A0258B079600F6F690 /* DataEncryption.swift in Sources */, B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */, B626A76D29928B1600053070 /* TestsClosureNavigationResponder.swift in Sources */, @@ -11317,7 +11290,6 @@ B66260E029AC6EBD00E9E3EE /* HistoryTabExtension.swift in Sources */, B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, - 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11497,7 +11469,7 @@ 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, - 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, + 4BCF15D92ABB8A7F0083F6DF /* SurveyRemoteMessage.swift in Sources */, 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, 4B9DB0292A983B24000927DB /* WaitlistStorage.swift in Sources */, AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */, @@ -11521,7 +11493,7 @@ 85707F31276A7DCA00DC0649 /* OnboardingViewModel.swift in Sources */, 85AC3B0525D6B1D800C7D2AA /* ScriptSourceProviding.swift in Sources */, 4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */, - 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, + 4BCF15D72ABB8A110083F6DF /* SurveyRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, F1C70D7C2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, @@ -11765,7 +11737,7 @@ 9833913327AAAEEE00DAF119 /* EmbeddedTrackerDataTests.swift in Sources */, 3776583127F8325B009A6B35 /* AutofillPreferencesTests.swift in Sources */, B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, - 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, + 4BD57C042AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, @@ -11856,7 +11828,7 @@ AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 4B723E0726B0003E00E14D75 /* CSVImporterTests.swift in Sources */, - 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.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/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json new file mode 100644 index 0000000000..85ed0dc199 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "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 new file mode 100644 index 0000000000..18085b7a09 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf differ diff --git a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift index 823ee28e4b..0e32253ec0 100644 --- a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift +++ b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift @@ -21,32 +21,20 @@ import Networking protocol HomePageRemoteMessagingRequest { - func fetchHomePageRemoteMessages(completion: @escaping (Result<[T], Error>) -> Void) + func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error> } final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingRequest { - enum NetworkProtectionEndpoint { + enum SurveysEndpoint { case debug case production var url: URL { switch self { - case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/network-protection/messages-v2-debug.json")! - case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/network-protection/messages-v2.json")! - } - } - } - - enum DataBrokerProtectionEndpoint { - case debug - case production - - var url: URL { - switch self { - case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/dbp/messages-debug.json")! - case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/dbp/messages.json")! + 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")! } } } @@ -56,19 +44,11 @@ final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingReques case requestCompletedWithoutErrorOrResponse } - static func networkProtectionMessagesRequest() -> HomePageRemoteMessagingRequest { + static func surveysRequest() -> HomePageRemoteMessagingRequest { #if DEBUG || REVIEW - return DefaultHomePageRemoteMessagingRequest(endpointURL: NetworkProtectionEndpoint.debug.url) + return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.debug.url) #else - return DefaultHomePageRemoteMessagingRequest(endpointURL: NetworkProtectionEndpoint.production.url) -#endif - } - - static func dataBrokerProtectionMessagesRequest() -> HomePageRemoteMessagingRequest { -#if DEBUG || REVIEW - return DefaultHomePageRemoteMessagingRequest(endpointURL: DataBrokerProtectionEndpoint.debug.url) -#else - return DefaultHomePageRemoteMessagingRequest(endpointURL: DataBrokerProtectionEndpoint.production.url) + return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.production.url) #endif } @@ -78,25 +58,27 @@ final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingReques self.endpointURL = endpointURL } - func fetchHomePageRemoteMessages(completion: @escaping (Result<[T], Error>) -> Void) { + 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) - request.fetch { response, error in - if let error { - completion(Result.failure(error)) - } else if let responseData = response?.data { - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode([T].self, from: responseData) - completion(Result.success(decoded)) - } catch { - completion(.failure(HomePageRemoteMessagingRequestError.failedToDecodeMessages)) - } - } else { - completion(.failure(HomePageRemoteMessagingRequestError.requestCompletedWithoutErrorOrResponse)) + 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 d9b1fd01ab..7057913dad 100644 --- a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift +++ b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift @@ -18,26 +18,21 @@ import Foundation -protocol HomePageRemoteMessagingStorage { +protocol SurveyRemoteMessagingStorage { - func store(messages: [Message]) throws - func storedMessages() -> [Message] + func store(messages: [SurveyRemoteMessage]) throws + func storedMessages() -> [SurveyRemoteMessage] func dismissRemoteMessage(with id: String) func dismissedMessageIDs() -> [String] } -final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorage { +final class DefaultSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage { - enum NetworkProtectionConstants { - static let dismissedMessageIdentifiersKey = "home.page.network-protection.dismissed-message-identifiers" - static let networkProtectionMessagesFileName = "network-protection-messages.json" - } - - enum DataBrokerProtectionConstants { - static let dismissedMessageIdentifiersKey = "home.page.dbp.dismissed-message-identifiers" - static let networkProtectionMessagesFileName = "dbp-messages.json" + enum SurveyConstants { + static let dismissedMessageIdentifiersKey = "home.page.survey.dismissed-message-identifiers" + static let surveyMessagesFileName = "survey-messages.json" } private let userDefaults: UserDefaults @@ -48,23 +43,16 @@ final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorag URL.sandboxApplicationSupportURL } - static func networkProtection() -> DefaultHomePageRemoteMessagingStorage { - return DefaultHomePageRemoteMessagingStorage( - messagesFileName: NetworkProtectionConstants.networkProtectionMessagesFileName, - dismissedMessageIdentifiersKey: NetworkProtectionConstants.dismissedMessageIdentifiersKey - ) - } - - static func dataBrokerProtection() -> DefaultHomePageRemoteMessagingStorage { - return DefaultHomePageRemoteMessagingStorage( - messagesFileName: DataBrokerProtectionConstants.networkProtectionMessagesFileName, - dismissedMessageIdentifiersKey: DataBrokerProtectionConstants.dismissedMessageIdentifiersKey + static func surveys() -> DefaultSurveyRemoteMessagingStorage { + return DefaultSurveyRemoteMessagingStorage( + messagesFileName: SurveyConstants.surveyMessagesFileName, + dismissedMessageIdentifiersKey: SurveyConstants.dismissedMessageIdentifiersKey ) } init( userDefaults: UserDefaults = .standard, - messagesDirectoryURL: URL = DefaultHomePageRemoteMessagingStorage.applicationSupportURL, + messagesDirectoryURL: URL = DefaultSurveyRemoteMessagingStorage.applicationSupportURL, messagesFileName: String, dismissedMessageIdentifiersKey: String ) { @@ -73,15 +61,15 @@ final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorag self.dismissedMessageIdentifiersKey = dismissedMessageIdentifiersKey } - func store(messages: [Message]) throws { + func store(messages: [SurveyRemoteMessage]) throws { let encoded = try JSONEncoder().encode(messages) try encoded.write(to: messagesURL) } - func storedMessages() -> [Message] { + func storedMessages() -> [SurveyRemoteMessage] { do { let messagesData = try Data(contentsOf: messagesURL) - let messages = try JSONDecoder().decode([Message].self, from: messagesData) + let messages = try JSONDecoder().decode([SurveyRemoteMessage].self, from: messagesData) return messages } catch { diff --git a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift similarity index 59% rename from DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift rename to DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift index a49c997ca0..b9fb348f24 100644 --- a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift +++ b/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionRemoteMessage.swift +// SurveyRemoteMessage.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,10 +18,10 @@ import Foundation import Common +import Subscription -struct DataBrokerRemoteMessageAction: Codable, Equatable, Hashable { +struct SurveyRemoteMessageAction: Codable, Equatable, Hashable { enum Action: String, Codable { - case openDataBrokerProtection case openSurveyURL case openURL } @@ -31,23 +31,31 @@ struct DataBrokerRemoteMessageAction: Codable, Equatable, Hashable { let actionURL: String? } -struct DataBrokerProtectionRemoteMessage: Codable, Equatable, Hashable { +struct SurveyRemoteMessage: Codable, Equatable, Identifiable, Hashable { + + struct Attributes: Codable, Equatable, Hashable { + let subscriptionStatus: String? + let subscriptionBillingPeriod: String? + let minimumDaysSinceSubscriptionStarted: Int? + let maximumDaysUntilSubscriptionExpirationOrRenewal: Int? + let daysSinceVPNEnabled: Int? + let daysSincePIREnabled: Int? + } let id: String let cardTitle: String let cardDescription: String - /// If this is set, the message won't be displayed if DBP hasn't been used, even if the usage and access booleans are false - let daysSinceDataBrokerProtectionEnabled: Int? - let requiresDataBrokerProtectionUsage: Bool - let requiresDataBrokerProtectionAccess: Bool - let action: DataBrokerRemoteMessageAction + let attributes: Attributes + let action: SurveyRemoteMessageAction func presentableSurveyURL( statisticsStore: StatisticsStore = LocalStatisticsStore(), - activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), + 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 + 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 @@ -62,8 +70,11 @@ struct DataBrokerProtectionRemoteMessage: Codable, Equatable, Hashable { operatingSystemVersion: operatingSystemVersion, appVersion: appVersion, hardwareModel: hardwareModel, - daysSinceActivation: activationDateStore.daysSinceActivation(), - daysSinceLastActive: activationDateStore.daysSinceLastActive() + 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 new file mode 100644 index 0000000000..51c1496884 --- /dev/null +++ b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift @@ -0,0 +1,270 @@ +// +// 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: AccountManaging + private let subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching + private let vpnActivationDateStore: WaitlistActivationDateStore + private let pirActivationDateStore: WaitlistActivationDateStore + private let minimumRefreshInterval: TimeInterval + private let userDefaults: UserDefaults + + convenience init(subscriptionManager: SubscriptionManaging) { + #if DEBUG || REVIEW + self.init( + accountManager: subscriptionManager.accountManager, + subscriptionFetcher: subscriptionManager.subscriptionService, + minimumRefreshInterval: .seconds(30) + ) + #else + self.init( + accountManager: subscriptionManager.accountManager, + subscriptionFetcher: subscriptionManager.subscriptionService, + minimumRefreshInterval: .hours(1) + ) + #endif + } + + init( + messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.surveysRequest(), + messageStorage: SurveyRemoteMessagingStorage = DefaultSurveyRemoteMessagingStorage.surveys(), + accountManager: AccountManaging, + subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching, + vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), + pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), + networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + 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 function_body_length + + /// 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 + + // 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 + } + } + + return attributeMatchStatus + + } + } + + // swiftlint:enable cyclomatic_complexity function_body_length + + 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) + } + +} + +extension SubscriptionService: SurveyRemoteMessageSubscriptionFetching { + + func getSubscription(accessToken: String) async -> Result { + return await self.getSubscription(accessToken: accessToken, cachePolicy: .returnCacheDataElseLoad) + } + +} diff --git a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift index a431276800..7392cb7f75 100644 --- a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift +++ b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift @@ -19,41 +19,60 @@ import Foundation import Common import BrowserServicesKit +import Subscription final class SurveyURLBuilder { enum SurveyURLParameters: String, CaseIterable { case atb = "atb" case atbVariant = "var" - case daysSinceActivated = "delta" - case macOSVersion = "mv" + case macOSVersion = "osv" case appVersion = "ddgv" case hardwareModel = "mo" - case lastDayActive = "da" + + 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 daysSinceActivation: Int? - private let daysSinceLastActive: Int? + 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?, - daysSinceActivation: Int?, - daysSinceLastActive: Int?) { + subscription: Subscription?, + daysSinceVPNActivated: Int?, + daysSinceVPNLastActive: Int?, + daysSincePIRActivated: Int?, + daysSincePIRLastActive: Int?) { self.statisticsStore = statisticsStore self.operatingSystemVersion = operatingSystemVersion self.appVersion = appVersion self.hardwareModel = hardwareModel - self.daysSinceActivation = daysSinceActivation - self.daysSinceLastActive = daysSinceLastActive + self.subscription = subscription + self.daysSinceVPNActivated = daysSinceVPNActivated + self.daysSinceVPNLastActive = daysSinceVPNLastActive + self.daysSincePIRActivated = daysSincePIRActivated + self.daysSincePIRLastActive = daysSincePIRLastActive } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length func buildSurveyURL(from originalURLString: String) -> URL? { guard var components = URLComponents(string: originalURLString) else { assertionFailure("Could not build components from survey URL") @@ -72,10 +91,6 @@ final class SurveyURLBuilder { if let variant = statisticsStore.variant { queryItems.append(queryItem(parameter: parameter, value: variant)) } - case .daysSinceActivated: - if let daysSinceActivation { - queryItems.append(queryItem(parameter: parameter, value: daysSinceActivation)) - } case .macOSVersion: queryItems.append(queryItem(parameter: parameter, value: operatingSystemVersion)) case .appVersion: @@ -84,9 +99,57 @@ final class SurveyURLBuilder { if let hardwareModel = hardwareModel { queryItems.append(queryItem(parameter: parameter, value: hardwareModel)) } - case .lastDayActive: - if let daysSinceLastActive { - queryItems.append(queryItem(parameter: parameter, value: daysSinceLastActive)) + 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)) } } } @@ -132,4 +195,12 @@ final class SurveyURLBuilder { return bucket } + 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/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift b/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift deleted file mode 100644 index b43994834b..0000000000 --- a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// DataBrokerProtectionRemoteMessaging.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 - -#if DBP - -protocol DataBrokerProtectionRemoteMessaging { - - func fetchRemoteMessages(completion: (() -> Void)?) - func presentableRemoteMessages() -> [DataBrokerProtectionRemoteMessage] - func dismiss(message: DataBrokerProtectionRemoteMessage) - -} - -final class DefaultDataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging { - - enum Constants { - static let lastRefreshDateKey = "data-broker-protection.remote-messaging.last-refresh-date" - } - - private let messageRequest: HomePageRemoteMessagingRequest - private let messageStorage: HomePageRemoteMessagingStorage - private let waitlistStorage: WaitlistStorage - private let waitlistActivationDateStore: WaitlistActivationDateStore - private let dataBrokerProtectionVisibility: DataBrokerProtectionFeatureVisibility - private let minimumRefreshInterval: TimeInterval - private let userDefaults: UserDefaults - - convenience init() { - #if DEBUG || REVIEW - self.init(minimumRefreshInterval: .seconds(30)) - #else - self.init(minimumRefreshInterval: .hours(1)) - #endif - } - - init( - messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.dataBrokerProtectionMessagesRequest(), - messageStorage: HomePageRemoteMessagingStorage = DefaultHomePageRemoteMessagingStorage.dataBrokerProtection(), - waitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: "dbp", keychainAppGroup: Bundle.main.appGroup(bundle: .dbp)), - waitlistActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), - dataBrokerProtectionVisibility: DataBrokerProtectionFeatureVisibility = DefaultDataBrokerProtectionFeatureVisibility(), - minimumRefreshInterval: TimeInterval, - userDefaults: UserDefaults = .standard - ) { - self.messageRequest = messageRequest - self.messageStorage = messageStorage - self.waitlistStorage = waitlistStorage - self.waitlistActivationDateStore = waitlistActivationDateStore - self.dataBrokerProtectionVisibility = dataBrokerProtectionVisibility - self.minimumRefreshInterval = minimumRefreshInterval - self.userDefaults = userDefaults - } - - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - if let lastRefreshDate = lastRefreshDate(), lastRefreshDate.addingTimeInterval(minimumRefreshInterval) > Date() { - fetchCompletion?() - return - } - - self.messageRequest.fetchHomePageRemoteMessages { [weak self] result in - defer { - fetchCompletion?() - } - - guard let self else { return } - - // Cast the generic parameter to a concrete type: - let result: Result<[DataBrokerProtectionRemoteMessage], Error> = result - - switch result { - case .success(let messages): - do { - try self.messageStorage.store(messages: messages) - self.updateLastRefreshDate() // Update last refresh date on success, otherwise let the app try again next time - } catch { - PixelKit.fire(DebugEvent(GeneralPixel.dataBrokerProtectionRemoteMessageStorageFailed, 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() // Avoid refreshing constantly when the file isn't available - return - } - - PixelKit.fire(DebugEvent(GeneralPixel.dataBrokerProtectionRemoteMessageFetchingFailed, error: error)) - } - } - } - - /// Uses the "days since DBP activated" count combined with the set of dismissed messages to determine which messages should be displayed to the user. - func presentableRemoteMessages() -> [DataBrokerProtectionRemoteMessage] { - let dismissedMessageIDs = messageStorage.dismissedMessageIDs() - let possibleMessages: [DataBrokerProtectionRemoteMessage] = messageStorage.storedMessages() - - // Only show messages that haven't been dismissed, and check whether they have a requirement on how long the user has used DBP for. - let filteredMessages = possibleMessages.filter { message in - - // Don't show messages that have already been dismissed. If you need to show the same message to a user again, - // it should get a new message ID. - if dismissedMessageIDs.contains(message.id) { - return false - } - - // First, check messages that require a number of days of DBP usage - if let requiredDaysSinceActivation = message.daysSinceDataBrokerProtectionEnabled, - let daysSinceActivation = waitlistActivationDateStore.daysSinceActivation() { - if requiredDaysSinceActivation <= daysSinceActivation { - return true - } else { - return false - } - } - - // Next, check if the message requires access to DBP but it's not visible: - if message.requiresDataBrokerProtectionAccess, !dataBrokerProtectionVisibility.isFeatureVisible() { - return false - } - - // Finally, check if the message requires DBP usage, and check if the user has used it at all: - if message.requiresDataBrokerProtectionUsage, waitlistActivationDateStore.daysSinceActivation() == nil { - return false - } - - return true - - } - - return filteredMessages - } - - func dismiss(message: DataBrokerProtectionRemoteMessage) { - 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") - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - return nil - } - - return date - } - - private func updateLastRefreshDate() { - userDefaults.setValue(Date(), forKey: Constants.lastRefreshDateKey) - } - -} - -#endif diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index df7ea99ccb..9522183014 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Common import Foundation import PixelKit +import Subscription import NetworkProtection import NetworkProtectionUI @@ -39,7 +40,7 @@ extension HomePage.Models { let gridWidth = FeaturesGridDimensions.width let deleteActionTitle = UserText.newTabSetUpRemoveItemAction let privacyConfigurationManager: PrivacyConfigurationManaging - let homePageRemoteMessaging: HomePageRemoteMessaging + let surveyRemoteMessaging: SurveyRemoteMessaging let permanentSurveyManager: SurveyManager var duckPlayerURL: String { @@ -53,6 +54,7 @@ extension HomePage.Models { private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager private let duckPlayerPreferences: DuckPlayerPreferencesPersistor + private let subscriptionManager: SubscriptionManaging @UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) var shouldShowAllFeatures: Bool { @@ -109,18 +111,20 @@ extension HomePage.Models { tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), duckPlayerPreferences: DuckPlayerPreferencesPersistor, - homePageRemoteMessaging: HomePageRemoteMessaging, + surveyRemoteMessaging: SurveyRemoteMessaging, privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, - permanentSurveyManager: SurveyManager = PermanentSurveyManager()) { + permanentSurveyManager: SurveyManager = PermanentSurveyManager(), + subscriptionManager: SubscriptionManaging = Application.appDelegate.subscriptionManager) { self.defaultBrowserProvider = defaultBrowserProvider self.dockCustomizer = dockCustomizer self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager self.duckPlayerPreferences = duckPlayerPreferences - self.homePageRemoteMessaging = homePageRemoteMessaging + self.surveyRemoteMessaging = surveyRemoteMessaging self.privacyConfigurationManager = privacyConfigurationManager self.permanentSurveyManager = permanentSurveyManager + self.subscriptionManager = subscriptionManager refreshFeaturesMatrix() @@ -142,9 +146,7 @@ extension HomePage.Models { performEmailProtectionAction() case .permanentSurvey: visitSurvey() - case .networkProtectionRemoteMessage(let message): - handle(remoteMessage: message) - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): handle(remoteMessage: message) case .dataBrokerProtectionWaitlistInvited: performDataBrokerProtectionWaitlistInvitedAction() @@ -205,14 +207,9 @@ extension HomePage.Models { shouldShowEmailProtectionSetting = false case .permanentSurvey: shouldShowPermanentSurvey = false - case .networkProtectionRemoteMessage(let message): - homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: message) - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDismissed(messageID: message.id)) - case .dataBrokerProtectionRemoteMessage(let message): -#if DBP - homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: message) - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageDismissed(messageID: message.id)) -#endif + case .surveyRemoteMessage(let message): + surveyRemoteMessaging.dismiss(message: message) + PixelKit.fire(GeneralPixel.surveyRemoteMessageDismissed(messageID: message.id)) case .dataBrokerProtectionWaitlistInvited: shouldShowDBPWaitlistInvitedCardUI = false } @@ -221,20 +218,16 @@ extension HomePage.Models { func refreshFeaturesMatrix() { var features: [FeatureType] = [] -#if DBP + if shouldDBPWaitlistCardBeVisible { features.append(.dataBrokerProtectionWaitlistInvited) } - for message in homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.presentableRemoteMessages() { - features.append(.dataBrokerProtectionRemoteMessage(message)) - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) + for message in surveyRemoteMessaging.presentableRemoteMessages() { + features.append(.surveyRemoteMessage(message)) + PixelKit.fire(GeneralPixel.surveyRemoteMessageDisplayed(messageID: message.id), frequency: .daily) } -#endif - for message in homePageRemoteMessaging.networkProtectionRemoteMessaging.presentableRemoteMessages() { - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) - } appendFeatureCards(&features) featuresMatrix = features.chunked(into: itemsPerRow) @@ -260,8 +253,7 @@ extension HomePage.Models { return shouldEmailProtectionCardBeVisible case .permanentSurvey: return shouldPermanentSurveyBeVisible - case .networkProtectionRemoteMessage, - .dataBrokerProtectionRemoteMessage, + case .surveyRemoteMessage, .dataBrokerProtectionWaitlistInvited: return false // These are handled separately } @@ -347,7 +339,8 @@ extension HomePage.Models { private var shouldPermanentSurveyBeVisible: Bool { return shouldShowPermanentSurvey && - permanentSurveyManager.isSurveyAvailable + permanentSurveyManager.isSurveyAvailable && + surveyRemoteMessaging.presentableRemoteMessages().isEmpty // When Privacy Pro survey is visible, ensure we do not show multiple at once } @MainActor private func visitSurvey() { @@ -358,55 +351,44 @@ extension HomePage.Models { shouldShowPermanentSurvey = false } - @MainActor private func handle(remoteMessage: NetworkProtectionRemoteMessage) { + @MainActor private func handle(remoteMessage: SurveyRemoteMessage) { guard let actionType = remoteMessage.action.actionType else { - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDismissed(messageID: remoteMessage.id)) - homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: remoteMessage) + PixelKit.fire(GeneralPixel.surveyRemoteMessageDismissed(messageID: remoteMessage.id)) + surveyRemoteMessaging.dismiss(message: remoteMessage) refreshFeaturesMatrix() return } switch actionType { - case .openNetworkProtection: - NotificationCenter.default.post(name: .ToggleNetworkProtectionInMainWindow, object: nil) case .openSurveyURL, .openURL: - if let surveyURL = remoteMessage.presentableSurveyURL() { - let tab = Tab(content: .url(surveyURL, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageOpened(messageID: remoteMessage.id)) - - // Dismiss the message after the user opens the URL, even if they just close the tab immediately afterwards. - homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() + Task { @MainActor in + var subscription: Subscription? + + if let token = subscriptionManager.accountManager.accessToken { + switch await subscriptionManager.subscriptionService.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() + } } } } - @MainActor private func handle(remoteMessage: DataBrokerProtectionRemoteMessage) { -#if DBP - guard let actionType = remoteMessage.action.actionType else { - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageDismissed(messageID: remoteMessage.id)) - homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() - return - } - - switch actionType { - case .openDataBrokerProtection: - break // Not used currently - case .openSurveyURL, .openURL: - if let surveyURL = remoteMessage.presentableSurveyURL() { - let tab = Tab(content: .url(surveyURL, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageOpened(messageID: remoteMessage.id)) - - // Dismiss the message after the user opens the URL, even if they just close the tab immediately afterwards. - homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() - } - } -#endif - } } // MARK: Feature Type @@ -429,8 +411,7 @@ extension HomePage.Models { case dock case importBookmarksAndPasswords case permanentSurvey - case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage) - case dataBrokerProtectionRemoteMessage(DataBrokerProtectionRemoteMessage) + case surveyRemoteMessage(SurveyRemoteMessage) case dataBrokerProtectionWaitlistInvited var title: String { @@ -447,9 +428,7 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionCardTitle case .permanentSurvey: return PermanentSurveyManager.title - case .networkProtectionRemoteMessage(let message): - return message.cardTitle - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): return message.cardTitle case .dataBrokerProtectionWaitlistInvited: return "Personal Information Removal" @@ -470,9 +449,7 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionSummary case .permanentSurvey: return PermanentSurveyManager.body - case .networkProtectionRemoteMessage(let message): - return message.cardDescription - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): return message.cardDescription case .dataBrokerProtectionWaitlistInvited: return "You're invited to try Personal Information Removal beta!" @@ -493,9 +470,7 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionAction case .permanentSurvey: return PermanentSurveyManager.actionTitle - case .networkProtectionRemoteMessage(let message): - return message.action.actionTitle - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): return message.action.actionTitle case .dataBrokerProtectionWaitlistInvited: return "Get Started" @@ -527,10 +502,8 @@ extension HomePage.Models { return .inbox128.resized(to: iconSize)! case .permanentSurvey: return .survey128.resized(to: iconSize)! - case .networkProtectionRemoteMessage: - return .vpnEnded.resized(to: iconSize)! - case .dataBrokerProtectionRemoteMessage: - return .dbpInformationRemover.resized(to: iconSize)! + case .surveyRemoteMessage: + return .privacyProSurvey.resized(to: iconSize)! case .dataBrokerProtectionWaitlistInvited: return .dbpInformationRemover.resized(to: iconSize)! } @@ -553,34 +526,6 @@ extension HomePage.Models { // MARK: - Remote Messaging -struct HomePageRemoteMessaging { - - static func defaultMessaging() -> HomePageRemoteMessaging { -#if DBP - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: .netP, - dataBrokerProtectionRemoteMessaging: DefaultDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: .dbp - ) -#else - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: .netP - ) -#endif - } - - let networkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging - let networkProtectionUserDefaults: UserDefaults - -#if DBP - let dataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging - let dataBrokerProtectionUserDefaults: UserDefaults -#endif - -} - extension AppVersion { public var majorAndMinorOSVersion: String { let components = osVersion.split(separator: ".") diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 092a33384c..224f0f93f4 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -154,7 +154,9 @@ final class HomePageViewController: NSViewController { dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), - homePageRemoteMessaging: .defaultMessaging() + surveyRemoteMessaging: DefaultSurveyRemoteMessaging( + subscriptionManager: Application.appDelegate.subscriptionManager + ) ) } diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 113e525dca..8d4c5852a9 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -192,13 +192,8 @@ final class MainViewController: NSViewController { updateReloadMenuItem() updateStopMenuItem() browserTabViewController.windowDidBecomeKey() - - refreshNetworkProtectionMessages() - -#if DBP + refreshSurveyMessages() DataBrokerProtectionAppEvents().windowDidBecomeMain() - refreshDataBrokerProtectionMessages() -#endif } func windowDidResignKey() { @@ -220,19 +215,15 @@ final class MainViewController: NSViewController { } } - private let networkProtectionMessaging = DefaultNetworkProtectionRemoteMessaging() - - func refreshNetworkProtectionMessages() { - networkProtectionMessaging.fetchRemoteMessages() - } - -#if DBP - private let dataBrokerProtectionMessaging = DefaultDataBrokerProtectionRemoteMessaging() + private lazy var surveyMessaging: DefaultSurveyRemoteMessaging = { + return DefaultSurveyRemoteMessaging(subscriptionManager: Application.appDelegate.subscriptionManager) + }() - func refreshDataBrokerProtectionMessages() { - dataBrokerProtectionMessaging.fetchRemoteMessages() + func refreshSurveyMessages() { + Task { + await surveyMessaging.fetchRemoteMessages() + } } -#endif 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 3a41e65525..c7e3b6d6ae 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -642,6 +642,10 @@ import SubscriptionUI currentViewController: { WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController }, 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 cef3e05a90..898f51ea74 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -927,6 +927,11 @@ 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/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 3cd6f11e63..23fb71b7ab 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -76,9 +76,6 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Remove Network Extension and Login Items", action: #selector(NetworkProtectionDebugMenu.removeSystemExtensionAndAgents)) .targetting(self) - - NSMenuItem(title: "Reset Remote Messages", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionRemoteMessages)) - .targetting(self) } NSMenuItem.separator() @@ -483,11 +480,6 @@ final class NetworkProtectionDebugMenu: NSMenu { overrideNetworkProtectionActivationDate(to: nil) } - @objc func resetNetworkProtectionRemoteMessages(_ sender: Any?) { - DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() - DefaultNetworkProtectionRemoteMessaging(minimumRefreshInterval: 0).resetLastRefreshTimestamp() - } - @objc func overrideNetworkProtectionActivationDateToNow(_ sender: Any?) { overrideNetworkProtectionActivationDate(to: Date()) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index cf810d9225..aa2c99a004 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -59,8 +59,6 @@ final class NetworkProtectionDebugUtilities { settings.resetToDefaults() - DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) UserDefaults.netP.networkProtectionEntitlementsExpired = false diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift deleted file mode 100644 index 2f5f6ce6fc..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// NetworkProtectionRemoteMessage.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 - -struct NetworkProtectionRemoteMessageAction: Codable, Equatable, Hashable { - enum Action: String, Codable { - case openNetworkProtection - case openSurveyURL - case openURL - } - - let actionTitle: String - let actionType: Action? - let actionURL: String? -} - -struct NetworkProtectionRemoteMessage: Codable, Equatable, Identifiable, Hashable { - - let id: String - let cardTitle: String - let cardDescription: String - /// If this is set, the message won't be displayed if NetP hasn't been used, even if the usage and access booleans are false - let daysSinceNetworkProtectionEnabled: Int? - let requiresNetworkProtectionUsage: Bool - let requiresNetworkProtectionAccess: Bool - let action: NetworkProtectionRemoteMessageAction - - func presentableSurveyURL( - statisticsStore: StatisticsStore = LocalStatisticsStore(), - activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description, - appVersion: String = AppVersion.shared.versionNumber, - hardwareModel: String? = HardwareModel.model - ) -> 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, - daysSinceActivation: activationDateStore.daysSinceActivation(), - daysSinceLastActive: activationDateStore.daysSinceLastActive() - ) - - return surveyURLBuilder.buildSurveyURL(from: surveyURL) - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift deleted file mode 100644 index aef4e46516..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// NetworkProtectionRemoteMessaging.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 - -protocol NetworkProtectionRemoteMessaging { - - func fetchRemoteMessages(completion: (() -> Void)?) - func presentableRemoteMessages() -> [NetworkProtectionRemoteMessage] - func dismiss(message: NetworkProtectionRemoteMessage) - -} - -final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging { - - enum Constants { - static let lastRefreshDateKey = "network-protection.remote-messaging.last-refresh-date" - } - - private let messageRequest: HomePageRemoteMessagingRequest - private let messageStorage: HomePageRemoteMessagingStorage - private let waitlistStorage: WaitlistStorage - private let waitlistActivationDateStore: WaitlistActivationDateStore - private let networkProtectionVisibility: NetworkProtectionFeatureVisibility - private let minimumRefreshInterval: TimeInterval - private let userDefaults: UserDefaults - - convenience init() { - #if DEBUG || REVIEW - self.init(minimumRefreshInterval: .seconds(30)) - #else - self.init(minimumRefreshInterval: .hours(1)) - #endif - } - - init( - messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.networkProtectionMessagesRequest(), - messageStorage: HomePageRemoteMessagingStorage = DefaultHomePageRemoteMessagingStorage.networkProtection(), - waitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: "networkprotection", keychainAppGroup: Bundle.main.appGroup(bundle: .netP)), - waitlistActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), - minimumRefreshInterval: TimeInterval, - userDefaults: UserDefaults = .standard - ) { - self.messageRequest = messageRequest - self.messageStorage = messageStorage - self.waitlistStorage = waitlistStorage - self.waitlistActivationDateStore = waitlistActivationDateStore - self.networkProtectionVisibility = networkProtectionVisibility - self.minimumRefreshInterval = minimumRefreshInterval - self.userDefaults = userDefaults - } - - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - - if let lastRefreshDate = lastRefreshDate(), lastRefreshDate.addingTimeInterval(minimumRefreshInterval) > Date() { - fetchCompletion?() - return - } - - self.messageRequest.fetchHomePageRemoteMessages { [weak self] result in - defer { - fetchCompletion?() - } - - guard let self else { return } - - // Cast the generic parameter to a concrete type: - let result: Result<[NetworkProtectionRemoteMessage], Error> = result - - switch result { - case .success(let messages): - do { - try self.messageStorage.store(messages: messages) - self.updateLastRefreshDate() // Update last refresh date on success, otherwise let the app try again next time - } catch { - PixelKit.fire(DebugEvent(GeneralPixel.networkProtectionRemoteMessageStorageFailed, 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() // Avoid refreshing constantly when the file isn't available - return - } - - PixelKit.fire(DebugEvent(GeneralPixel.networkProtectionRemoteMessageFetchingFailed, error: error)) - } - } - - } - - /// Uses the "days since VPN activated" count combined with the set of dismissed messages to determine which messages should be displayed to the user. - func presentableRemoteMessages() -> [NetworkProtectionRemoteMessage] { - let dismissedMessageIDs = messageStorage.dismissedMessageIDs() - let possibleMessages: [NetworkProtectionRemoteMessage] = messageStorage.storedMessages() - - // Only show messages that haven't been dismissed, and check whether they have a - // requirement on how long the user has used the VPN for. - let filteredMessages = possibleMessages.filter { message in - - // Don't show messages that have already been dismissed. If you need to show the same message to a user again, - // it should get a new message ID. - if dismissedMessageIDs.contains(message.id) { - return false - } - - // First, check messages that require a number of days of NetP usage - if let requiredDaysSinceActivation = message.daysSinceNetworkProtectionEnabled, - let daysSinceActivation = waitlistActivationDateStore.daysSinceActivation() { - if requiredDaysSinceActivation <= daysSinceActivation { - return true - } else { - return false - } - } - - // Next, check if the message requires access to NetP but it's not visible: - if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isVPNVisible() { - return false - } - - // Finally, check if the message requires NetP usage, and check if the user has used it at all: - if message.requiresNetworkProtectionUsage, waitlistActivationDateStore.daysSinceActivation() == nil { - return false - } - - return true - - } - - return filteredMessages - } - - func dismiss(message: NetworkProtectionRemoteMessage) { - 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") - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - return nil - } - - return date - } - - private func updateLastRefreshDate() { - userDefaults.setValue(Date(), forKey: Constants.lastRefreshDateKey) - } - -} diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift index e7e56043b8..39914a5dad 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -212,8 +212,11 @@ final class AutofillPreferencesModel: ObservableObject { operatingSystemVersion: operatingSystemVersion, appVersion: appVersion, hardwareModel: hardwareModel, - daysSinceActivation: activationDateStore.daysSinceActivation(), - daysSinceLastActive: activationDateStore.daysSinceLastActive() + subscription: nil, + daysSinceVPNActivated: nil, + daysSinceVPNLastActive: nil, + daysSincePIRActivated: nil, + daysSincePIRLastActive: nil ) guard let surveyUrl = surveyURLBuilder.buildSurveyURLWithPasswordsCountSurveyParameter(from: "https://selfserve.decipherinc.com/survey/selfserve/32ab/240307") else { diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 6184263d85..42fb1feab6 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -117,13 +117,14 @@ 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) - // VPN - case networkProtectionRemoteMessageDisplayed(messageID: String) - case networkProtectionRemoteMessageDismissed(messageID: String) - case networkProtectionRemoteMessageOpened(messageID: String) case networkProtectionEnabledOnSearch case networkProtectionGeoswitchingOpened case networkProtectionGeoswitchingSetNearest @@ -152,9 +153,6 @@ enum GeneralPixel: PixelKitEventV2 { case dataBrokerProtectionWaitlistCardUITapped case dataBrokerProtectionWaitlistTermsAndConditionsDisplayed case dataBrokerProtectionWaitlistTermsAndConditionsAccepted - case dataBrokerProtectionRemoteMessageDisplayed(messageID: String) - case dataBrokerProtectionRemoteMessageDismissed(messageID: String) - case dataBrokerProtectionRemoteMessageOpened(messageID: String) // Login Item events case dataBrokerEnableLoginItemDaily @@ -343,11 +341,8 @@ enum GeneralPixel: PixelKitEventV2 { case burnerTabMisplaced - case networkProtectionRemoteMessageFetchingFailed - case networkProtectionRemoteMessageStorageFailed - case dataBrokerProtectionRemoteMessageFetchingFailed - case dataBrokerProtectionRemoteMessageStorageFailed - + case surveyRemoteMessageFetchingFailed + case surveyRemoteMessageStorageFailed case loginItemUpdateError(loginItemBundleID: String, action: String, buildType: String, osVersion: String) // Tracks installation without tracking retention. @@ -530,12 +525,12 @@ enum GeneralPixel: PixelKitEventV2 { case .vpnBreakageReport: return "m_mac_vpn_breakage_report" - case .networkProtectionRemoteMessageDisplayed(let messageID): - return "m_mac_netp_remote_message_displayed_\(messageID)" - case .networkProtectionRemoteMessageDismissed(let messageID): - return "m_mac_netp_remote_message_dismissed_\(messageID)" - case .networkProtectionRemoteMessageOpened(let messageID): - return "m_mac_netp_remote_message_opened_\(messageID)" + 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" @@ -575,12 +570,6 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_dbp_imp_terms" case .dataBrokerProtectionWaitlistTermsAndConditionsAccepted: return "m_mac_dbp_ev_terms_accepted" - case .dataBrokerProtectionRemoteMessageDisplayed(let messageID): - return "m_mac_dbp_remote_message_displayed_\(messageID)" - case .dataBrokerProtectionRemoteMessageDismissed(let messageID): - return "m_mac_dbp_remote_message_dismissed_\(messageID)" - case .dataBrokerProtectionRemoteMessageOpened(let messageID): - return "m_mac_dbp_remote_message_opened_\(messageID)" case .dataBrokerEnableLoginItemDaily: return "m_mac_dbp_daily_login-item_enable" case .dataBrokerDisableLoginItemDaily: return "m_mac_dbp_daily_login-item_disable" @@ -863,12 +852,8 @@ enum GeneralPixel: PixelKitEventV2 { case .burnerTabMisplaced: return "burner_tab_misplaced" - case .networkProtectionRemoteMessageFetchingFailed: return "netp_remote_message_fetching_failed" - case .networkProtectionRemoteMessageStorageFailed: return "netp_remote_message_storage_failed" - - case .dataBrokerProtectionRemoteMessageFetchingFailed: return "dbp_remote_message_fetching_failed" - case .dataBrokerProtectionRemoteMessageStorageFailed: return "dbp_remote_message_storage_failed" - + 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/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index cd6186444d..eebcb0f3d9 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -50,10 +50,7 @@ let extraInputFiles: [TargetName: Set] = [ "Unit Tests": [ .init("BWEncryptionTests.swift", .source), - .init("WKWebViewPrivateMethodsAvailabilityTests.swift", .source), - .init("NetworkProtectionRemoteMessageTests.swift", .source), - .init("network-protection-messages.json", .resource), - .init("dbp-messages.json", .resource), + .init("WKWebViewPrivateMethodsAvailabilityTests.swift", .source) ], "Integration Tests": [] diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 9d49ab4073..0986fc1150 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -21,42 +21,22 @@ import BrowserServicesKit import Common @testable import DuckDuckGo_Privacy_Browser -final class MockNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging { +final class MockSurveyRemoteMessaging: SurveyRemoteMessaging { - var messages: [NetworkProtectionRemoteMessage] = [] + var messages: [SurveyRemoteMessage] = [] - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - fetchCompletion?() + func fetchRemoteMessages() async { + return } - func presentableRemoteMessages() -> [NetworkProtectionRemoteMessage] { + func presentableRemoteMessages() -> [SurveyRemoteMessage] { messages } - func dismiss(message: NetworkProtectionRemoteMessage) {} + func dismiss(message: SurveyRemoteMessage) {} } -#if DBP - -final class MockDataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging { - - var messages: [DataBrokerProtectionRemoteMessage] = [] - - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - fetchCompletion?() - } - - func presentableRemoteMessages() -> [DataBrokerProtectionRemoteMessage] { - messages - } - - func dismiss(message: DataBrokerProtectionRemoteMessage) {} - -} - -#endif - final class ContinueSetUpModelTests: XCTestCase { var vm: HomePage.Models.ContinueSetUpModel! @@ -70,6 +50,7 @@ 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() { @@ -87,22 +68,9 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() + mockSurveyMessaging = MockSurveyRemoteMessaging() dockCustomizer = DockCustomizerMock() -#if DBP - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults, - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: userDefaults - ) -#else - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults - ) -#endif - vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, @@ -110,7 +78,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: messaging, + surveyRemoteMessaging: mockSurveyMessaging, privacyConfigurationManager: privacyConfigManager, permanentSurveyManager: MockPermanentSurveyManager() ) @@ -154,7 +122,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: createMessaging(), + surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -368,7 +336,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: createMessaging(), + surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -467,7 +435,7 @@ final class ContinueSetUpModelTests: XCTestCase { XCTAssertFalse(vm2.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.permanentSurvey)) } - @MainActor func testWhenAskedToPerformActionForPermanetShowsTheSurveySite() async { + @MainActor func testWhenAskedToPerformActionForPermanentShowsTheSurveySite() async { let expectedURL = URL(string: "someurl.com") let surveyManager = MockPermanentSurveyManager(isSurveyAvailable: true, url: expectedURL) userDefaults.set(true, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) @@ -478,7 +446,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: createMessaging(), + surveyRemoteMessaging: createMessaging(), privacyConfigurationManager: privacyConfigManager, permanentSurveyManager: surveyManager ) @@ -509,20 +477,8 @@ final class ContinueSetUpModelTests: XCTestCase { return features.chunked(into: HomePage.featuresPerRow) } - private func createMessaging() -> HomePageRemoteMessaging { -#if DBP - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults, - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: userDefaults - ) -#else - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults - ) -#endif + private func createMessaging() -> SurveyRemoteMessaging { + MockSurveyRemoteMessaging() } @MainActor func test_WhenUserDoesntHaveApplicationInTheDock_ThenAddToDockCardIsDisplayed() { @@ -566,20 +522,6 @@ extension HomePage.Models.ContinueSetUpModel { let manager = MockPrivacyConfigurationManager() manager.privacyConfig = privacyConfig -#if DBP - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: appGroupUserDefaults, - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: appGroupUserDefaults - ) -#else - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: appGroupUserDefaults - ) -#endif - return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: defaultBrowserProvider, dockCustomizer: dockCustomizer, @@ -587,7 +529,7 @@ extension HomePage.Models.ContinueSetUpModel { tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: messaging, + surveyRemoteMessaging: MockSurveyRemoteMessaging(), privacyConfigurationManager: manager, permanentSurveyManager: permanentSurveyManager) } diff --git a/UnitTests/HomePage/Resources/survey-messages.json b/UnitTests/HomePage/Resources/survey-messages.json new file mode 100644 index 0000000000..1259271b49 --- /dev/null +++ b/UnitTests/HomePage/Resources/survey-messages.json @@ -0,0 +1,20 @@ +[ + { + "id": "message-1", + "cardTitle": "Title 1", + "cardDescription": "Description 1", + "attributes": { + "subscriptionStatus": "Auto-Renewable", + "subscriptionBillingPeriod": "Monthly", + "minimumDaysSinceSubscriptionStarted": 1, + "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, + "daysSinceVPNEnabled": 2, + "daysSincePIREnabled": 3 + }, + "action": { + "actionTitle": "Action 1", + "actionType": "openSurveyURL", + "actionURL": "https://duckduckgo.com/" + } + } +] diff --git a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessageTests.swift b/UnitTests/HomePage/SurveyRemoteMessageTests.swift similarity index 52% rename from UnitTests/NetworkProtection/NetworkProtectionRemoteMessageTests.swift rename to UnitTests/HomePage/SurveyRemoteMessageTests.swift index 1a791b92e9..1c7939b36f 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessageTests.swift +++ b/UnitTests/HomePage/SurveyRemoteMessageTests.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionRemoteMessageTests.swift +// SurveyRemoteMessageTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,7 +19,7 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser -final class NetworkProtectionRemoteMessageTests: XCTestCase { +final class SurveyRemoteMessageTests: XCTestCase { func testWhenDecodingMessages_ThenMessagesDecodeSuccessfully() throws { let mockStatisticsStore = MockStatisticsStore() @@ -34,66 +34,32 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { let data = try Data(contentsOf: fileURL) let decoder = JSONDecoder() - let decodedMessages = try decoder.decode([NetworkProtectionRemoteMessage].self, from: data) + let decodedMessages = try decoder.decode([SurveyRemoteMessage].self, from: data) - XCTAssertEqual(decodedMessages.count, 3) + XCTAssertEqual(decodedMessages.count, 1) - guard let firstMessage = decodedMessages.first(where: { $0.id == "123"}) else { + guard let firstMessage = decodedMessages.first(where: { $0.id == "message-1"}) else { XCTFail("Failed to find expected message") return } let firstMessagePresentableSurveyURL = firstMessage.presentableSurveyURL( statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, + vpnActivationDateStore: mockActivationDateStore, operatingSystemVersion: "1.2.3", appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" + hardwareModel: "MacBookPro,123", + subscription: nil ) XCTAssertEqual(firstMessage.cardTitle, "Title 1") XCTAssertEqual(firstMessage.cardDescription, "Description 1") XCTAssertEqual(firstMessage.action.actionTitle, "Action 1") - XCTAssertNil(firstMessagePresentableSurveyURL) - XCTAssertNil(firstMessage.daysSinceNetworkProtectionEnabled) - - guard let secondMessage = decodedMessages.first(where: { $0.id == "456"}) else { - XCTFail("Failed to find expected message") - return - } - - let secondMessagePresentableSurveyURL = secondMessage.presentableSurveyURL( - statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, - operatingSystemVersion: "1.2.3", - appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" - ) - - XCTAssertEqual(secondMessage.daysSinceNetworkProtectionEnabled, 1) - XCTAssertEqual(secondMessage.cardTitle, "Title 2") - XCTAssertEqual(secondMessage.cardDescription, "Description 2") - XCTAssertEqual(secondMessage.action.actionTitle, "Action 2") - XCTAssertNil(secondMessagePresentableSurveyURL) - - guard let thirdMessage = decodedMessages.first(where: { $0.id == "789"}) else { - XCTFail("Failed to find expected message") - return - } - - let thirdMessagePresentableSurveyURL = thirdMessage.presentableSurveyURL( - statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, - operatingSystemVersion: "1.2.3", - appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" - ) - - XCTAssertEqual(thirdMessage.daysSinceNetworkProtectionEnabled, 5) - XCTAssertEqual(thirdMessage.cardTitle, "Title 3") - XCTAssertEqual(thirdMessage.cardDescription, "Description 3") - XCTAssertEqual(thirdMessage.action.actionTitle, "Action 3") - XCTAssertTrue(thirdMessagePresentableSurveyURL!.absoluteString.hasPrefix("https://duckduckgo.com/")) + XCTAssertEqual(firstMessage.attributes.minimumDaysSinceSubscriptionStarted, 1) + XCTAssertEqual(firstMessage.attributes.daysSinceVPNEnabled, 2) + XCTAssertEqual(firstMessage.attributes.daysSincePIREnabled, 3) + XCTAssertEqual(firstMessage.attributes.maximumDaysUntilSubscriptionExpirationOrRenewal, 30) + XCTAssertNotNil(firstMessagePresentableSurveyURL) } func testWhenGettingSurveyURL_AndSurveyURLHasParameters_ThenParametersAreReplaced() { @@ -103,8 +69,13 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { "daysSinceNetworkProtectionEnabled": 0, "cardTitle": "Title", "cardDescription": "Description", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, + "attributes": { + "subscriptionStatus": "", + "minimumDaysSinceSubscriptionStarted": 1, + "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, + "daysSinceVPNEnabled": 1, + "daysSincePIREnabled": 1 + }, "action": { "actionTitle": "Action", "actionType": "openSurveyURL", @@ -114,9 +85,9 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { """ let decoder = JSONDecoder() - let message: NetworkProtectionRemoteMessage + let message: SurveyRemoteMessage do { - message = try decoder.decode(NetworkProtectionRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) + message = try decoder.decode(SurveyRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) } catch { XCTFail("Failed to decode with error: \(error.localizedDescription)") return @@ -132,19 +103,23 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { let presentableSurveyURL = message.presentableSurveyURL( statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, + vpnActivationDateStore: mockActivationDateStore, operatingSystemVersion: "1.2.3", appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" + hardwareModel: "MacBookPro,123", + subscription: nil ) - let expectedURL = "https://duckduckgo.com/?atb=atb-123&var=variant&delta=2&mv=1.2.3&ddgv=4.5.6&mo=MacBookPro%252C123&da=1" + 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: NetworkProtectionRemoteMessageTests.self) - return bundle.resourceURL!.appendingPathComponent("network-protection-messages.json") + 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 new file mode 100644 index 0000000000..09a6f43fbb --- /dev/null +++ b/UnitTests/HomePage/SurveyRemoteMessagingTests.swift @@ -0,0 +1,296 @@ +// +// 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: AccountManaging! + private var subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching! + + override func setUp() { + defaults = UserDefaults(suiteName: testGroupName)! + defaults.removePersistentDomain(forName: testGroupName) + + accountManager = AccountManagerMock(isUserAuthenticated: true, 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_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]) + } + + private func mockMessage(id: String, + subscriptionStatus: String = Subscription.Status.autoRenewable.rawValue, + minimumDaysSinceSubscriptionStarted: Int = 0, + maximumDaysUntilSubscriptionExpirationOrRenewal: Int = 0, + daysSinceVPNEnabled: Int = 0, + daysSincePIREnabled: Int = 0) -> SurveyRemoteMessage { + let remoteMessageJSON = """ + { + "id": "\(id)", + "cardTitle": "Title", + "cardDescription": "Description 1", + "attributes": { + "subscriptionStatus": "\(subscriptionStatus)", + "minimumDaysSinceSubscriptionStarted": \(minimumDaysSinceSubscriptionStarted), + "maximumDaysUntilSubscriptionExpirationOrRenewal": \(maximumDaysUntilSubscriptionExpirationOrRenewal), + "daysSinceVPNEnabled": \(daysSinceVPNEnabled), + "daysSincePIREnabled": \(daysSincePIREnabled) + }, + "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) + } + +} diff --git a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift b/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift deleted file mode 100644 index 3f96a7d0ea..0000000000 --- a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift +++ /dev/null @@ -1,376 +0,0 @@ -// -// NetworkProtectionRemoteMessagingTests.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 NetworkProtectionRemoteMessagingTests: XCTestCase { - - private var defaults: UserDefaults! - private let testGroupName = "remote-messaging" - - override func setUp() { - defaults = UserDefaults(suiteName: testGroupName)! - defaults.removePersistentDomain(forName: testGroupName) - } - - func testWhenFetchingRemoteMessages_AndTheUserDidNotSignUpViaWaitlist_ThenMessagesAreFetched() { - let request = MockNetworkProtectionRemoteMessagingRequest() - request.result = .success([]) - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertTrue(!waitlistStorage.isWaitlistUser) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertTrue(request.didFetchMessages) - } - - func testWhenFetchingRemoteMessages_AndTheUserDidSignUpViaWaitlist_ButUserHasNotActivatedNetP_ThenMessagesAreFetched() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - request.result = .success([]) - waitlistStorage.store(waitlistToken: "token") - waitlistStorage.store(waitlistTimestamp: 123) - waitlistStorage.store(inviteCode: "ABCD1234") - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertTrue(waitlistStorage.isWaitlistUser) - XCTAssertNil(activationDateStorage.daysSinceActivation()) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertTrue(request.didFetchMessages) - } - - func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ThenMessagesAreFetched_AndMessagesAreStored() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let messages = [mockMessage(id: "123")] - - request.result = .success(messages) - waitlistStorage.store(waitlistToken: "token") - waitlistStorage.store(waitlistTimestamp: 123) - waitlistStorage.store(inviteCode: "ABCD1234") - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertTrue(waitlistStorage.isWaitlistUser) - XCTAssertEqual(storage.storedMessages(), []) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertTrue(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), messages) - } - - func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ButRateLimitedOperationCannotRunAgain_ThenMessagesAreNotFetched() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - waitlistStorage.store(waitlistToken: "token") - waitlistStorage.store(waitlistTimestamp: 123) - waitlistStorage.store(inviteCode: "ABCD1234") - activationDateStorage._daysSinceActivation = 10 - - defaults.setValue(Date(), forKey: DefaultNetworkProtectionRemoteMessaging.Constants.lastRefreshDateKey) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: .days(7), // Use a large number to hit the refresh check - userDefaults: defaults - ) - - XCTAssertTrue(waitlistStorage.isWaitlistUser) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertFalse(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), []) - } - - func testWhenStoredMessagesExist_AndSomeMessagesHaveBeenDismissed_ThenPresentableMessagesDoNotIncludeDismissedMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let dismissedMessage = mockMessage(id: "123") - let activeMessage = mockMessage(id: "456") - try? storage.store(messages: [dismissedMessage, activeMessage]) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - 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_AndSomeMessagesRequireDaysActive_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let hiddenMessage = mockMessage(id: "123", daysSinceNetworkProtectionEnabled: 10) - let activeMessage = mockMessage(id: "456") - try? storage.store(messages: [hiddenMessage, activeMessage]) - activationDateStorage._daysSinceActivation = 5 - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessagesAfter = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessagesAfter, [activeMessage]) - } - - func testWhenStoredMessagesExist_AndSomeMessagesNetPVisibility_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) - - let hiddenMessage = mockMessage(id: "123", requiresNetPAccess: true) - try? storage.store(messages: [hiddenMessage]) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessages = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessages, []) - } - - func testWhenStoredMessagesExist_AndSomeMessagesRequireNetPUsage_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let message = mockMessage(id: "123", requiresNetPUsage: false, requiresNetPAccess: true) - try? storage.store(messages: [message]) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessages = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessages, [message]) - } - - private func mockMessage(id: String, - daysSinceNetworkProtectionEnabled: Int = 0, - requiresNetPUsage: Bool = true, - requiresNetPAccess: Bool = true) -> NetworkProtectionRemoteMessage { - let remoteMessageJSON = """ - { - "id": "\(id)", - "daysSinceNetworkProtectionEnabled": \(daysSinceNetworkProtectionEnabled), - "cardTitle": "Title", - "cardDescription": "Description", - "surveyURL": "https://duckduckgo.com/", - "requiresNetworkProtectionUsage": \(String(describing: requiresNetPUsage)), - "requiresNetworkProtectionAccess": \(String(describing: requiresNetPAccess)), - "action": { - "actionTitle": "Action" - } - } - """ - - let decoder = JSONDecoder() - return try! decoder.decode(NetworkProtectionRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) - } - -} - -// MARK: - Mocks - -private final class MockNetworkProtectionRemoteMessagingRequest: HomePageRemoteMessagingRequest { - - var result: Result<[NetworkProtectionRemoteMessage], Error>! - var didFetchMessages: Bool = false - - func fetchHomePageRemoteMessages(completion: @escaping (Result<[T], Error>) -> Void) where T: Decodable { - didFetchMessages = true - - if let castResult = self.result as? Result<[T], Error> { - completion(castResult) - } else { - fatalError("Could not cast result to expected type") - } - } - -} - -private final class MockNetworkProtectionRemoteMessagingStorage: HomePageRemoteMessagingStorage { - - var _storedMessages: [NetworkProtectionRemoteMessage] = [] - var _storedDismissedMessageIDs: [String] = [] - - func store(messages: [NetworkProtectionRemoteMessage]) throws { - self._storedMessages = messages - } - - func storedMessages() -> [NetworkProtectionRemoteMessage] { - _storedMessages - } - - func store(messages: [Message]) throws { - if let messages = messages as? [NetworkProtectionRemoteMessage] { - self._storedMessages = messages - } else { - fatalError("Failed to cast messages") - } - } - - func storedMessages() -> [Message] { - return _storedMessages as! [Message] - } - - 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 - } - -} diff --git a/UnitTests/NetworkProtection/Resources/dbp-messages.json b/UnitTests/NetworkProtection/Resources/dbp-messages.json deleted file mode 100644 index 8d7a6dc7dc..0000000000 --- a/UnitTests/NetworkProtection/Resources/dbp-messages.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "id": "123", - "cardTitle": "Title 1", - "cardDescription": "Description 1", - "requiresDataBrokerProtectionAccess": true, - "requiresDataBrokerProtectionUsage": true, - "action": { - "actionTitle": "Action 1" - } - }, - { - "id": "456", - "daysSinceDataBrokerProtectionEnabled": 1, - "cardTitle": "Title 2", - "cardDescription": "Description 2", - "requiresDataBrokerProtectionAccess": true, - "requiresDataBrokerProtectionUsage": true, - "action": { - "actionTitle": "Action 2" - } - }, - { - "id": "789", - "daysSinceDataBrokerProtectionEnabled": 5, - "cardTitle": "Title 3", - "cardDescription": "Description 3", - "requiresDataBrokerProtectionAccess": true, - "requiresDataBrokerProtectionUsage": true, - "action": { - "actionTitle": "Action 3", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - - } -] diff --git a/UnitTests/NetworkProtection/Resources/network-protection-messages.json b/UnitTests/NetworkProtection/Resources/network-protection-messages.json deleted file mode 100644 index d69450766f..0000000000 --- a/UnitTests/NetworkProtection/Resources/network-protection-messages.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "id": "123", - "cardTitle": "Title 1", - "cardDescription": "Description 1", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, - "action": { - "actionTitle": "Action 1" - } - }, - { - "id": "456", - "daysSinceNetworkProtectionEnabled": 1, - "cardTitle": "Title 2", - "cardDescription": "Description 2", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, - "action": { - "actionTitle": "Action 2" - } - }, - { - "id": "789", - "daysSinceNetworkProtectionEnabled": 5, - "cardTitle": "Title 3", - "cardDescription": "Description 3", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, - "action": { - "actionTitle": "Action 3", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - - } -]