From 5473b36a9b5ef3c5bd4e669e565f6207e8693d8e Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 3 Jan 2025 16:41:29 -0300 Subject: [PATCH] Implement tab bar remote message (#3665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204006570077678/1208970712110808/f Tech Design URL: CC: **Description** Adds a new remote message that is shown in the tab bar. **Steps to test this PR**: 1. Go to the `RemoteMessagingClient` and change the DEBUG endpoint URL to `https://www.jsonblob.com/api/1316017217598578688` 2. Run the app 3. You should see the new blue button in the tab bar 4. When hovering it, it should show a popup. 5. Tapping the button will take you to the survey 6. Test that when the button is dismissed is removed in all windows 7. Test that New Tab Page does not show this new message **Definition of Done**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Dominik Kapusta --- DuckDuckGo.xcodeproj/project.pbxproj | 36 ++++ .../dax-response.imageset/Contents.json | 12 ++ .../Response-DDG-Question-96x96.svg | 20 ++ DuckDuckGo/HomePage/View/HomePageView.swift | 4 +- .../MainWindow/MainViewController.swift | 2 +- .../ActiveRemoteMessageModel+NewTabPage.swift | 6 +- .../ActiveRemoteMessageModel.swift | 26 ++- .../TabBarActiveRemoteMessage.swift | 52 +++++ .../TabBarRemoteMessageView.swift | 118 +++++++++++ .../TabBar/Model/TabBarRemoteMessage.swift | 26 +++ .../View/TabBarRemoteMessagePresenting.swift | 191 ++++++++++++++++++ .../TabBar/View/TabBarViewController.swift | 22 +- .../TabBarRemoteMessageViewModel.swift | 89 ++++++++ ...wTabPageActiveRemoteMessageProviding.swift | 4 +- .../NewTabPage/RMF/NewTabPageRMFClient.swift | 14 +- ...ewTabPageActiveRemoteMessageProvider.swift | 6 +- .../NewTabPageRMFClientTests.swift | 28 +-- .../ActiveRemoteMessageModelTests.swift | 41 +++- .../TabBarRemoteMessageViewModelTests.swift | 159 +++++++++++++++ 19 files changed, 817 insertions(+), 39 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg create mode 100644 DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift create mode 100644 DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift create mode 100644 DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift create mode 100644 DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift create mode 100644 DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift create mode 100644 UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 46c507b66a..26d6473310 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2875,6 +2875,8 @@ B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; }; B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; }; BB0346F52CEB80B400D23E05 /* DownloadsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0346F42CEB80B400D23E05 /* DownloadsTests.swift */; }; + BB3229052D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */; }; + BB3229062D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */; }; BB4339DB2C7F9606005D7ED7 /* PinnedTabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4339DA2C7F9606005D7ED7 /* PinnedTabsTests.swift */; }; BB470EBB2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */; }; BB470EBC2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */; }; @@ -2883,6 +2885,16 @@ BB731F312CDBA6360023D2E4 /* FireWindowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */; }; BB7B5F982C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; + BB7BA64C2D11FC170020B47E /* TabBarRemoteMessagePresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresenting.swift */; }; + BB7BA64D2D11FC170020B47E /* TabBarRemoteMessagePresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresenting.swift */; }; + BB9BA2202D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */; }; + BB9BA2212D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */; }; + BB9BA2262D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */; }; + BB9BA2272D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */; }; + BB9BA2292D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */; }; + BB9BA22A2D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */; }; + BB9BDD492D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */; }; + BB9BDD4A2D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */; }; BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; BBB881892C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */; }; @@ -4852,12 +4864,18 @@ B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB0346F42CEB80B400D23E05 /* DownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTests.swift; sourceTree = ""; }; + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageView.swift; sourceTree = ""; }; BB4339DA2C7F9606005D7ED7 /* PinnedTabsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTabsTests.swift; sourceTree = ""; }; BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewModel.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BB5F46A22C8751F6005F72DF /* BookmarkSortTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSortTests.swift; sourceTree = ""; }; BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireWindowTests.swift; sourceTree = ""; }; BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSearchAndSortMetrics.swift; sourceTree = ""; }; + BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessagePresenting.swift; sourceTree = ""; }; + BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModelTests.swift; sourceTree = ""; }; + BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessage.swift; sourceTree = ""; }; + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarActiveRemoteMessage.swift; sourceTree = ""; }; + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModel.swift; sourceTree = ""; }; BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerSearchDataSource.swift; sourceTree = ""; }; BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchTests.swift; sourceTree = ""; }; BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBookmarksViewModelTests.swift; sourceTree = ""; }; @@ -5783,6 +5801,8 @@ 3712091F2C232E2B003ADF3D /* RemoteMessaging */ = { isa = PBXGroup; children = ( + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */, + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */, 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */, 3768D8432C2CC884004120AE /* RemoteMessagingConfigMatcherProvider.swift */, 3768D83F2C29C1F1004120AE /* ActiveRemoteMessageModel.swift */, @@ -8327,6 +8347,7 @@ AA86491224D831A1001BABEE /* View */ = { isa = PBXGroup; children = ( + BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresenting.swift */, AA80EC7B256C46AA007083E7 /* TabBar.storyboard */, 1430DFF424D0580F00B8978C /* TabBarViewController.swift */, 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */, @@ -8464,6 +8485,7 @@ AA8EDF1F2491FCC10071C2E8 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */, 37D23779287EB8CA00BCE03B /* TabIndex.swift */, AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */, ); @@ -8565,6 +8587,7 @@ AA9FF95724A1ECE20039E328 /* Model */ = { isa = PBXGroup; children = ( + BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */, AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */, ); path = Model; @@ -8778,6 +8801,7 @@ AAC9C01A24CB592E00AD1325 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */, AAC9C01D24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift */, 37D23788288009CF00BCE03B /* TabCollectionViewModelTests+PinnedTabs.swift */, 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */, @@ -11320,6 +11344,7 @@ BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */, B60D644A2AAF1B7C00B26F50 /* AddressBarTextSelectionNavigation.swift in Sources */, 1D01A3D52B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, + BB9BA2262D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */, 37219B342CBFBBE800C9D7A8 /* NewTabPageSearchBoxExperiment.swift in Sources */, 3706FA98293F65D500E42796 /* SecureVaultSorting.swift in Sources */, 1DEDB3652C19934C006B6D1B /* MoreOptionsMenuButton.swift in Sources */, @@ -11341,6 +11366,7 @@ 7BFF35742C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 370270BD2C78B6D3002E44E4 /* NewTabBackgroundPixel.swift in Sources */, + BB7BA64C2D11FC170020B47E /* TabBarRemoteMessagePresenting.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, BD88A83F2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, @@ -11710,6 +11736,7 @@ C1C405882C7F80E50089DE8A /* PromotionView+FreemiumDBP.swift in Sources */, B6E1491029A5C30500AAFBE8 /* ContentBlockingTabExtension.swift in Sources */, 3706FB82293F65D500E42796 /* PasswordManagementNoteItemView.swift in Sources */, + BB9BDD492D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */, 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */, 3706FEC5293F6F0600E42796 /* BWInstallationService.swift in Sources */, BDBA85972C5D256C00BC54F5 /* VPNFeedbackFormView.swift in Sources */, @@ -11998,6 +12025,7 @@ F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */, 3706FC1F293F65D500E42796 /* BookmarksBarViewController.swift in Sources */, 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, + BB3229062D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */, 3706FC20293F65D500E42796 /* PreferencesAutofillView.swift in Sources */, CD2AB5C22C8222F50019EB49 /* MaliciousSiteProtectionPreferences.swift in Sources */, 3706FC21293F65D500E42796 /* UserText+PasswordManager.swift in Sources */, @@ -12147,6 +12175,7 @@ 3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */, 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */, 1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */, + BB9BA22A2D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */, 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, @@ -12348,6 +12377,7 @@ 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, + BB9BA2202D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */, C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 37D046A52C7DAA8900AEAA50 /* ImageProcessorMock.swift in Sources */, 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, @@ -12999,6 +13029,7 @@ B693955326F04BEC0015B914 /* WindowDraggingView.swift in Sources */, 4B1E6EED27AB5E5100F51793 /* SecureVaultSorting.swift in Sources */, 37CD54CE27F2FDD100F1F7B9 /* PreferencesSidebarModel.swift in Sources */, + BB9BA2272D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */, 1D43EB32292788C70065E5D6 /* BWEncryptionOutput.m in Sources */, 3707EC4A2C47E36A00B67CBE /* CloseButton.swift in Sources */, B6106BAD26A7BF390013B453 /* PermissionState.swift in Sources */, @@ -13145,6 +13176,7 @@ 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */, 56DB9FE92CD24B47001BEC23 /* ContextualOnboardingPixel.swift in Sources */, 37AFCE9227DB8CAD00471A10 /* PreferencesAboutView.swift in Sources */, + BB7BA64D2D11FC170020B47E /* TabBarRemoteMessagePresenting.swift in Sources */, F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 4B2F565C2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 9826B0A02747DF3D0092F683 /* ContentBlocking.swift in Sources */, @@ -13182,6 +13214,7 @@ 37D0469F2C7D0EDD00AEAA50 /* CustomBackground.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, + BB9BDD4A2D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */, AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */, 1DA84D2F2C11989D0011C80F /* Update.swift in Sources */, AAC30A2A268E239100D2D9CD /* CrashReport.swift in Sources */, @@ -13606,6 +13639,7 @@ AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */, EEC111E6294D06290086524F /* JSAlertViewModel.swift in Sources */, 4BE5336C286912D40019DBFD /* BookmarksBarCollectionViewItem.swift in Sources */, + BB9BA2292D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */, B6C0B23926E742610031CB7F /* FileDownloadError.swift in Sources */, 85589EA027BFE60E0038AD11 /* MoreOrLessView.swift in Sources */, B6CC26682BAD959500F53F8D /* DownloadProgress.swift in Sources */, @@ -13794,6 +13828,7 @@ 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */, B60C6F7729B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, B65E6B9E26D9EC0800095F96 /* CircularProgressView.swift in Sources */, + BB3229052D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */, 56A053FC2C19E8F7007D8FAB /* OnboardingActionsManager.swift in Sources */, EEE50C292C38249C003DD7FF /* OptionalExtension.swift in Sources */, AABEE69C24A902BB0043105B /* SuggestionContainer.swift in Sources */, @@ -14091,6 +14126,7 @@ B63ED0DC26AE7B1E00A9DAD1 /* WebViewMock.swift in Sources */, 56A053FF2C1AEFA1007D8FAB /* OnboardingManagerTests.swift in Sources */, 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */, + BB9BA2212D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */, C172E7332C93759C00521D9A /* SyncPromoManagerTests.swift in Sources */, AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */, 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json new file mode 100644 index 0000000000..50d23b7933 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Response-DDG-Question-96x96.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg new file mode 100644 index 0000000000..e3d009683d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/HomePage/View/HomePageView.swift b/DuckDuckGo/HomePage/View/HomePageView.swift index 44c71a6288..f6ddd2f35f 100644 --- a/DuckDuckGo/HomePage/View/HomePageView.swift +++ b/DuckDuckGo/HomePage/View/HomePageView.swift @@ -183,7 +183,9 @@ extension HomePage.Views { @ViewBuilder func remoteMessage() -> some View { - if let remoteMessage = activeRemoteMessageModel.remoteMessage, let modelType = remoteMessage.content, modelType.isSupported { + if let remoteMessage = activeRemoteMessageModel.newTabPageRemoteMessage, + let modelType = remoteMessage.content, + modelType.isSupported { ZStack { RemoteMessageView(viewModel: .init( messageId: remoteMessage.id, diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index d0674088c2..ffc06c00aa 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -75,7 +75,7 @@ final class MainViewController: NSViewController { self.isBurner = tabCollectionViewModel.isBurner self.featureFlagger = featureFlagger - tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) + tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, activeRemoteMessageModel: NSApp.delegateTyped.activeRemoteMessageModel) bookmarksBarVisibilityManager = BookmarksBarVisibilityManager(selectedTabPublisher: tabCollectionViewModel.$selectedTabViewModel.eraseToAnyPublisher()) let networkProtectionPopoverManager: NetPPopoverManager = { diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift index 26b617934d..1fd02423dd 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -21,8 +21,10 @@ import NewTabPage import RemoteMessaging extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { - var remoteMessagePublisher: AnyPublisher { - $remoteMessage.dropFirst().eraseToAnyPublisher() + var newTabPageRemoteMessagePublisher: AnyPublisher { + $newTabPageRemoteMessage + .dropFirst() + .eraseToAnyPublisher() } func isMessageSupported(_ message: RemoteMessageModel) -> Bool { diff --git a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift index 0af0f67f97..b49a6c19c8 100644 --- a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift +++ b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift @@ -35,7 +35,9 @@ import os.log */ final class ActiveRemoteMessageModel: ObservableObject { - @Published var remoteMessage: RemoteMessageModel? + @Published private var remoteMessage: RemoteMessageModel? + @Published var newTabPageRemoteMessage: RemoteMessageModel? + @Published var tabBarRemoteMessage: RemoteMessageModel? @Published var isViewOnScreen: Bool = false /** @@ -94,6 +96,21 @@ final class ActiveRemoteMessageModel: ObservableObject { } .store(in: &cancellables) + $remoteMessage + .sink { [weak self] newMessage in + if let newMessage = newMessage { + if newMessage.isForTabBar { + self?.tabBarRemoteMessage = newMessage + } else { + self?.newTabPageRemoteMessage = newMessage + } + } else { + self?.newTabPageRemoteMessage = nil + self?.tabBarRemoteMessage = nil + } + } + .store(in: &cancellables) + let remoteMessagePublisher = $remoteMessage .compactMap({ $0 }) .filter { [weak self] _ in self?.isViewOnScreen == true } @@ -185,3 +202,10 @@ extension RemoteMessageModelType { } } } + +private extension RemoteMessageModel { + + var isForTabBar: Bool { + return id == TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId + } +} diff --git a/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift b/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift new file mode 100644 index 0000000000..10c4ffa877 --- /dev/null +++ b/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift @@ -0,0 +1,52 @@ +// +// TabBarActiveRemoteMessage.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import RemoteMessaging + +protocol TabBarRemoteMessageProviding { + var remoteMessagePublisher: AnyPublisher { get } + + func markRemoteMessageAsShown() async + func onSurveyOpened() async + func onMessageDismissed() async +} + +final class TabBarActiveRemoteMessage: TabBarRemoteMessageProviding { + private let activeRemoteMessageModel: ActiveRemoteMessageModel + + var remoteMessagePublisher: AnyPublisher { + activeRemoteMessageModel.$tabBarRemoteMessage.eraseToAnyPublisher() + } + + init(activeRemoteMessageModel: ActiveRemoteMessageModel) { + self.activeRemoteMessageModel = activeRemoteMessageModel + } + + func markRemoteMessageAsShown() async { + await activeRemoteMessageModel.markRemoteMessageAsShown() + } + + func onSurveyOpened() async { + await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction) + } + + func onMessageDismissed() async { + await activeRemoteMessageModel.dismissRemoteMessage(with: .close) + } +} diff --git a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift new file mode 100644 index 0000000000..b08d56b530 --- /dev/null +++ b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift @@ -0,0 +1,118 @@ +// +// TabBarRemoteMessageView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct TabBarRemoteMessageView: View { + @State private var wasViewHovered: Bool = false + @State private var wasCloseButtonHovered: Bool = false + + let model: TabBarRemoteMessage + + let onClose: () -> Void + let onTap: (URL) -> Void + let onHover: () -> Void + let onHoverEnd: () -> Void + let onAppear: () -> Void + + var body: some View { + HStack(spacing: 0) { + HStack { + Text(model.buttonTitle) + .font(.system(size: 13)) + .fixedSize(horizontal: true, vertical: false) + .foregroundColor(.white) + } + .padding([.leading, .top, .bottom], 8) + .padding(.trailing, 6) + .cornerRadius(8) + .background(wasViewHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + .onTapGesture { onTap(model.surveyURL) } + .onHover { hovering in + wasViewHovered = hovering + + if hovering { + onHover() + } else { + onHoverEnd() + } + } + + Divider() + .background(Color.white.opacity(0.3)) + .frame(width: 1) + .padding([.top, .bottom], 3) + + HStack { + Image(.close) + .resizable() + .scaledToFit() + .foregroundColor(.white) + .frame(width: 16, height: 16) + } + .padding([.top, .bottom]) + .padding([.leading, .trailing], 4) + .background(wasCloseButtonHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + .cornerRadius(8) + .onTapGesture { + onClose() + } + .onHover { hovering in + wasCloseButtonHovered = hovering + } + .frame(maxWidth: .infinity) + } + .background(wasCloseButtonHovered || wasViewHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + .frame(height: 24) + .cornerRadius(8) + .onAppear(perform: { onAppear() }) + } +} + +struct TabBarRemoteMessagePopoverContent: View { + let model: TabBarRemoteMessage + + var body: some View { + HStack(alignment: .center, spacing: 12) { + Image(.daxResponse) + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + + VStack(alignment: .leading, spacing: 8) { + Text(model.popupTitle) + .font(.system(size: 13, weight: .bold)) + .padding(.top, 9) + + Text(model.popupSubtitle) + .font(.system(size: 13, weight: .medium)) + .padding(.bottom, 9) + } + } + .frame(width: 360) + .padding([.top, .bottom], 10) + .padding(.leading, 12) + .padding(.trailing, 24) + } +} diff --git a/DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift b/DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift new file mode 100644 index 0000000000..02aa30c1f3 --- /dev/null +++ b/DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift @@ -0,0 +1,26 @@ +// +// TabBarRemoteMessage.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +struct TabBarRemoteMessage { + static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar" + + let buttonTitle: String + let popupTitle: String + let popupSubtitle: String + let surveyURL: URL +} diff --git a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift new file mode 100644 index 0000000000..629341b1b1 --- /dev/null +++ b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift @@ -0,0 +1,191 @@ +// +// TabBarRemoteMessagePresenting.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +/// A protocol that defines the requirements for presenting tab bar remote messages in the macOS browser tab bar.. +/// +/// This protocol is designed for any class that needs to manage the display of remote messages in the tab bar. +/// It provides properties for managing the view model, popover, and UI components related to the remote message presentation. +/// +/// Properties: +/// - `tabBarRemoteMessageViewModel`: The view model responsible for managing the state and data of the tab bar remote message. +/// - `rightSideStackView`: The stack view that contains the UI elements on the right side of the tab bar. +/// - `tabBarRemoteMessagePopover`: An optional popover that displays additional information related to the remote message. +/// - `tabBarRemoteMessagePopoverHoverTimer`: An optional timer that controls the display of the popover based on user interaction. +/// - `feedbackBarButtonHostingController`: An optional hosting controller that manages the view for the feedback button associated with the remote message. +/// - `tabBarRemoteMessageCancellable`: An optional cancellable for the Combine publisher that listens for changes in the remote message state. +protocol TabBarRemoteMessagePresenting: AnyObject { + var tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel { get } + var rightSideStackView: NSStackView! { get } + var tabBarRemoteMessagePopover: NSPopover? { get set } + var tabBarRemoteMessagePopoverHoverTimer: Timer? { get set } + var feedbackBarButtonHostingController: NSHostingController? { get set } + var tabBarRemoteMessageCancellable: AnyCancellable? { get set } +} + +extension TabBarRemoteMessagePresenting { + + /// Adds a listener for changes in the remote message state. + /// + /// This method subscribes to the `remoteMessage` publisher of the `tabBarRemoteMessageViewModel`. + /// When a new remote message is received, it displays the message if the feedback button is not already shown. + /// If the remote message is nil, it removes the feedback button if it is currently displayed. + func addTabBarRemoteMessageListener() { + tabBarRemoteMessageCancellable = tabBarRemoteMessageViewModel.$remoteMessage + .sink(receiveValue: { tabBarRemoteMessage in + if let tabBarRemoteMessage = tabBarRemoteMessage { + if self.feedbackBarButtonHostingController == nil { + self.showTabBarRemoteMessage(tabBarRemoteMessage) + } + } else { + if self.feedbackBarButtonHostingController != nil { + self.removeFeedbackButton() + } + } + }) + } + + /// Displays the tab bar remote message in the UI. + /// + /// This method creates a `TabBarRemoteMessageView` with the provided remote message and sets up + /// actions for closing the message, tapping on it, and handling hover events. + /// The view is then inserted into the `rightSideStackView`. + /// + /// - Parameter tabBarRemotMessage: The remote message to be displayed. + private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { + let feedbackButtonView = TabBarRemoteMessageView( + model: tabBarRemotMessage, + onClose: { [weak self] in + guard let self = self else { return } + + self.tabBarRemoteMessageViewModel.onMessageDismissed() + self.removeFeedbackButton() + }, + onTap: { [weak self] surveyURL in + guard let self = self else { return } + + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) + } + self.tabBarRemoteMessageViewModel.onSurveyOpened() + self.removeFeedbackButton() + }, + onHover: { [weak self] in + guard let self = self else { return } + self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) + }, + onHoverEnd: { [weak self] in + guard let self = self else { return } + self.dismissTabBarRemoteMessagePopover() + }, + onAppear: { [weak self] in + guard let self = self else { return } + self.tabBarRemoteMessageViewModel.markTabBarRemoteMessageAsShown() + } + ) + feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) + guard let feedbackBarButtonHostingController else { return } + + feedbackBarButtonHostingController.view.translatesAutoresizingMaskIntoConstraints = false + + // Insert the hosting controller's view into the stack view just before the fire button + let index = max(0, rightSideStackView.arrangedSubviews.count - 1) + rightSideStackView.insertArrangedSubview(feedbackBarButtonHostingController.view, at: index) + + NSLayoutConstraint.activate([ + feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) + ]) + } + + /// Starts a timer to show the tab bar remote message popover after a delay. + /// + /// This method invalidates any existing timer and creates a new timer that will trigger the display + /// of the popover after a specified time interval. + /// + /// - Parameter message: The remote message associated with the popover + private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in + guard let self = self else { return } + self.showTabBarRemotePopup(message) + } + } + + /// Dismisses the tab bar remote message popover. + /// + /// This method invalidates the hover timer and closes the popover if it is currently displayed. + private func dismissTabBarRemoteMessagePopover() { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopover?.close() + } + + /// Shows the tab bar remote message popover. + /// + /// This method displays the popover containing the remote message. If the popover has not been created yet, + /// it initializes and configures it before displaying. + /// + /// - Parameter message: The remote message to be displayed in the popover. + private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + + if let popover = tabBarRemoteMessagePopover { + popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } else { + tabBarRemoteMessagePopover = NSPopover() + configurePopover(with: message) + + tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } + } + + /// Configures the popover with the specified remote message. + /// + /// This method sets the properties of the popover, including its size and content view controller, + /// which displays the remote message. + /// + /// - Parameter message: The remote message to configure the popover with. + private func configurePopover(with message: TabBarRemoteMessage) { + guard let popover = tabBarRemoteMessagePopover else { return } + + let contentView = TabBarRemoteMessagePopoverContent(model: message) + popover.animates = true + popover.behavior = .semitransient + popover.contentSize = NSHostingView(rootView: contentView).fittingSize + let controller = NSViewController() + controller.view = NSHostingView(rootView: contentView) + popover.contentViewController = controller + } + + /// Removes the feedback button from the UI. + /// + /// This method removes the feedback button's view from the `rightSideStackView` and cleans up + /// the associated hosting controller. + private func removeFeedbackButton() { + guard let hostingController = feedbackBarButtonHostingController else { return } + + rightSideStackView.removeArrangedSubview(hostingController.view) + hostingController.view.removeFromSuperview() + hostingController.removeFromParent() + feedbackBarButtonHostingController = nil + } + +} diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index f9038f0b94..766060ddd6 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -23,8 +23,9 @@ import Lottie import SwiftUI import WebKit import os.log +import RemoteMessaging -final class TabBarViewController: NSViewController { +final class TabBarViewController: NSViewController, TabBarRemoteMessagePresenting { enum HorizontalSpace: CGFloat { case pinnedTabsScrollViewPadding = 76 @@ -70,11 +71,17 @@ final class TabBarViewController: NSViewController { private let pinnedTabsViewModel: PinnedTabsViewModel? private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? - private var selectionIndexCancellable: AnyCancellable? private var mouseDownCancellable: AnyCancellable? private var cancellables = Set() + // TabBarRemoteMessagePresentable + var tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel + var tabBarRemoteMessagePopover: NSPopover? + var tabBarRemoteMessagePopoverHoverTimer: Timer? + var feedbackBarButtonHostingController: NSHostingController? + var tabBarRemoteMessageCancellable: AnyCancellable? + @IBOutlet weak var shadowView: TabShadowView! @IBOutlet weak var rightSideStackView: NSStackView! @@ -86,9 +93,9 @@ final class TabBarViewController: NSViewController { } } - static func create(tabCollectionViewModel: TabCollectionViewModel) -> TabBarViewController { + static func create(tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) -> TabBarViewController { NSStoryboard(name: "TabBar", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel) + self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, activeRemoteMessageModel: activeRemoteMessageModel) }! } @@ -96,8 +103,11 @@ final class TabBarViewController: NSViewController { fatalError("TabBarViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel) { + init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) { self.tabCollectionViewModel = tabCollectionViewModel + let tabBarActiveRemoteMessageModel = TabBarActiveRemoteMessage(activeRemoteMessageModel: activeRemoteMessageModel) + self.tabBarRemoteMessageViewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: tabBarActiveRemoteMessageModel, + isFireWindow: tabCollectionViewModel.isBurner) if !tabCollectionViewModel.isBurner, let pinnedTabCollection = tabCollectionViewModel.pinnedTabsManager?.tabCollection { let pinnedTabsViewModel = PinnedTabsViewModel(collection: pinnedTabCollection) let pinnedTabsView = PinnedTabsView(model: pinnedTabsViewModel) @@ -136,12 +146,14 @@ final class TabBarViewController: NSViewController { // Detect if tabs are clicked when the window is not in focus // https://app.asana.com/0/1177771139624306/1202033879471339 addMouseMonitors() + addTabBarRemoteMessageListener() } override func viewWillDisappear() { super.viewWillDisappear() mouseDownCancellable = nil + tabBarRemoteMessageCancellable = nil } override func viewDidLayout() { diff --git a/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift new file mode 100644 index 0000000000..4d2754a7ed --- /dev/null +++ b/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift @@ -0,0 +1,89 @@ +// +// TabBarRemoteMessageViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import RemoteMessaging + +final class TabBarRemoteMessageViewModel: ObservableObject { + + private let tabBarRemoteActiveMessage: TabBarRemoteMessageProviding + private var cancellable: AnyCancellable? + + @Published var remoteMessage: TabBarRemoteMessage? + + init(activeRemoteMessageModel: TabBarRemoteMessageProviding, isFireWindow: Bool) { + self.tabBarRemoteActiveMessage = activeRemoteMessageModel + + cancellable = tabBarRemoteActiveMessage.remoteMessagePublisher + .sink(receiveValue: { model in + guard !isFireWindow else { return } + + guard let model = model else { + self.remoteMessage = nil + return + } + + if model.shouldShowTabBarRemoteMessage, let tabBarRemoteMessage = model.mapToTabBarRemoteMessage() { + self.remoteMessage = tabBarRemoteMessage + } + }) + } + + func onSurveyOpened() { + Task { await tabBarRemoteActiveMessage.onSurveyOpened() } + } + + func onMessageDismissed() { + Task { await tabBarRemoteActiveMessage.onMessageDismissed() } + } + + func markTabBarRemoteMessageAsShown() { + Task { await tabBarRemoteActiveMessage.markRemoteMessageAsShown() } + } +} + +private extension RemoteMessageModel { + + var shouldShowTabBarRemoteMessage: Bool { + guard let modelType = content else { return false } + + return modelType.isSupported + } + + func mapToTabBarRemoteMessage() -> TabBarRemoteMessage? { + guard let modelType = content else { return nil } + + switch modelType { + case .bigSingleAction(let titleText, + let descriptionText, + _, + let primaryActionText, + let primaryAction): + + if case .survey(let value) = primaryAction, let surveyURL = URL(string: value) { + return .init(buttonTitle: titleText, + popupTitle: primaryActionText, + popupSubtitle: descriptionText, + surveyURL: surveyURL) + } else { + return nil + } + default: return nil + } + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift index 8035ae7d52..9fa892478a 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift @@ -20,8 +20,8 @@ import Combine import RemoteMessaging public protocol NewTabPageActiveRemoteMessageProviding { - var remoteMessage: RemoteMessageModel? { get set } - var remoteMessagePublisher: AnyPublisher { get } + var newTabPageRemoteMessage: RemoteMessageModel? { get set } + var newTabPageRemoteMessagePublisher: AnyPublisher { get } func isMessageSupported(_ message: RemoteMessageModel) -> Bool diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift index bb8d38a9ac..aa0ff4acc5 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift @@ -36,7 +36,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { public init(remoteMessageProvider: NewTabPageActiveRemoteMessageProviding) { self.remoteMessageProvider = remoteMessageProvider - remoteMessageProvider.remoteMessagePublisher + remoteMessageProvider.newTabPageRemoteMessagePublisher .sink { [weak self] remoteMessage in self?.notifyRemoteMessageDidChange(remoteMessage) } @@ -62,7 +62,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { @MainActor private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let remoteMessage = remoteMessageProvider.remoteMessage else { + guard let remoteMessage = remoteMessageProvider.newTabPageRemoteMessage else { return NewTabPageUserScript.RMFData(content: nil) } @@ -71,7 +71,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { private func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let remoteMessageParams: NewTabPageUserScript.RemoteMessageParams = DecodableHelper.decode(from: params), - remoteMessageParams.id == remoteMessageProvider.remoteMessage?.id + remoteMessageParams.id == remoteMessageProvider.newTabPageRemoteMessage?.id else { return nil } @@ -82,12 +82,12 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { private func primaryAction(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let remoteMessageParams: NewTabPageUserScript.RemoteMessageParams = DecodableHelper.decode(from: params), - remoteMessageParams.id == remoteMessageProvider.remoteMessage?.id + remoteMessageParams.id == remoteMessageProvider.newTabPageRemoteMessage?.id else { return nil } - switch remoteMessageProvider.remoteMessage?.content { + switch remoteMessageProvider.newTabPageRemoteMessage?.content { case let .bigSingleAction(_, _, _, _, primaryAction): await remoteMessageProvider.handleAction(primaryAction, andDismissUsing: .action) case let .bigTwoAction(_, _, _, _, primaryAction, _, _): @@ -100,12 +100,12 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { private func secondaryAction(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let remoteMessageParams: NewTabPageUserScript.RemoteMessageParams = DecodableHelper.decode(from: params), - remoteMessageParams.id == remoteMessageProvider.remoteMessage?.id + remoteMessageParams.id == remoteMessageProvider.newTabPageRemoteMessage?.id else { return nil } - switch remoteMessageProvider.remoteMessage?.content { + switch remoteMessageProvider.newTabPageRemoteMessage?.content { case let .bigTwoAction(_, _, _, _, _, _, secondaryAction): await remoteMessageProvider.handleAction(secondaryAction, andDismissUsing: .secondaryAction) default: diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift index 1cd2f3ccd1..165f036f1c 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift @@ -22,10 +22,10 @@ import XCTest import NewTabPage final class CapturingNewTabPageActiveRemoteMessageProvider: NewTabPageActiveRemoteMessageProviding { - @Published var remoteMessage: RemoteMessageModel? + @Published var newTabPageRemoteMessage: RemoteMessageModel? - var remoteMessagePublisher: AnyPublisher { - $remoteMessage.dropFirst().eraseToAnyPublisher() + var newTabPageRemoteMessagePublisher: AnyPublisher { + $newTabPageRemoteMessage.dropFirst().eraseToAnyPublisher() } func isMessageSupported(_ message: RemoteMessageModel) -> Bool { diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift index ae271591b3..53b6c23ba2 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift @@ -42,21 +42,21 @@ final class NewTabPageRMFClientTests: XCTestCase { // MARK: - getData func testThatGetDataReturnsSmallMessageIfPresent() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .small(.init(id: "sample_message", titleText: "title", descriptionText: "description"))) } func testThatGetDataReturnsMediumMessageIfPresent() async throws { - remoteMessageProvider.remoteMessage = .mockMedium(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockMedium(id: "sample_message") let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .medium(.init(id: "sample_message", titleText: "title", descriptionText: "description", icon: .criticalUpdate))) } func testThatGetDataReturnsBigSingleActionMessageIfPresent() async throws { - remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) + remoteMessageProvider.newTabPageRemoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .bigSingleAction( @@ -71,7 +71,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testThatGetDataReturnsBigTwoActionMessageIfPresent() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) + remoteMessageProvider.newTabPageRemoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .bigTwoAction( @@ -89,7 +89,7 @@ final class NewTabPageRMFClientTests: XCTestCase { // MARK: - dismiss func testThatDismissSendsDismissActionToProvider() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) @@ -97,7 +97,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenMessageIdDoesNotMatchThenDismissHasNoEffect() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) @@ -107,7 +107,7 @@ final class NewTabPageRMFClientTests: XCTestCase { // MARK: - primaryAction func testWhenSingleActionMessageThenPrimaryActionSendsActionToProvider() async throws { - remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) + remoteMessageProvider.newTabPageRemoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) @@ -115,7 +115,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenTwoActionMessageThenPrimaryActionSendsPrimaryActionToProvider() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) + remoteMessageProvider.newTabPageRemoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) @@ -123,7 +123,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenMessageHasNoButtonThenPrimaryActionHasNoEffect() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) @@ -131,7 +131,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenMessageIdDoesNotMatchThenPrimaryActionHasNoEffect() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) @@ -141,7 +141,7 @@ final class NewTabPageRMFClientTests: XCTestCase { // MARK: - secondaryAction func testWhenTwoActionMessageThenSecondaryActionSendsSecondaryActionToProvider() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .dismiss, secondaryAction: .appStore) + remoteMessageProvider.newTabPageRemoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .dismiss, secondaryAction: .appStore) let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) @@ -149,7 +149,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenSingleActionMessageThenSecondaryActionHasNoEffect() async throws { - remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) + remoteMessageProvider.newTabPageRemoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) @@ -157,7 +157,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenMessageHasNoButtonThenSecondaryActionHasNoEffect() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) @@ -165,7 +165,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenMessageIdDoesNotMatchThenSecondaryActionHasNoEffect() async throws { - remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") + remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) diff --git a/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift b/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift index ed21ad5d34..078a391177 100644 --- a/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift +++ b/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift @@ -43,7 +43,7 @@ final class ActiveRemoteMessageModelTests: XCTestCase { openURLHandler: { _ in } ) - XCTAssertNil(model.remoteMessage) + XCTAssertNil(model.newTabPageRemoteMessage) } func testWhenMessageIsScheduledThenItIsLoadedToModel() throws { @@ -54,7 +54,7 @@ final class ActiveRemoteMessageModelTests: XCTestCase { openURLHandler: { _ in } ) - XCTAssertEqual(model.remoteMessage, message) + XCTAssertEqual(model.newTabPageRemoteMessage, message) } func testWhenMessageIsDismissedThenItIsClearedFromModel() async throws { @@ -66,7 +66,7 @@ final class ActiveRemoteMessageModelTests: XCTestCase { ) await model.dismissRemoteMessage(with: .close) - XCTAssertNil(model.remoteMessage) + XCTAssertNil(model.newTabPageRemoteMessage) } func testWhenMessageIsMarkedAsShownThenShownFlagIsSavedInStore() async throws { @@ -81,4 +81,39 @@ final class ActiveRemoteMessageModelTests: XCTestCase { await model.markRemoteMessageAsShown() XCTAssertTrue(store.hasShownRemoteMessage(withID: message.id)) } + + func testWhenMessageIsForTabBar_thenCorrectPublisherIsSet() { + let tabBarRemoteMessage = RemoteMessageModel( + id: TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId, + content: .bigSingleAction(titleText: "Help Us Improve!", + descriptionText: "Description", + placeholder: .announce, + primaryActionText: "Test", + primaryAction: .survey(value: "www.survey.com")), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: false + ) + store.scheduledRemoteMessage = tabBarRemoteMessage + model = ActiveRemoteMessageModel( + remoteMessagingStore: self.store, + remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider(), + openURLHandler: { _ in } + ) + + XCTAssertNotNil(model.tabBarRemoteMessage) + XCTAssertNil(model.newTabPageRemoteMessage) + } + + func testWhenMessageIsForNewTabPage_thenCorrectPublisherIsSet() { + store.scheduledRemoteMessage = message + model = ActiveRemoteMessageModel( + remoteMessagingStore: self.store, + remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider(), + openURLHandler: { _ in } + ) + + XCTAssertNil(model.tabBarRemoteMessage) + XCTAssertNotNil(model.newTabPageRemoteMessage) + } } diff --git a/UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift new file mode 100644 index 0000000000..a1c691a58e --- /dev/null +++ b/UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift @@ -0,0 +1,159 @@ +// +// TabBarRemoteMessageViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +import RemoteMessaging +@testable import DuckDuckGo_Privacy_Browser + +class TabBarRemoteMessageViewModelTests: XCTestCase { + private var cancellables: Set = [] + + func testWhenModelIsNotForTabBar_thenIsNotSetAsRemoteMessage() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: false) + let expectation = XCTestExpectation(description: "Publisher should emit a nil value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage == nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createOtherRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenModelIsForTabBarButIsMalformed_thenIsNotSetAsRemoteMessage() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: false) + let expectation = XCTestExpectation(description: "Publisher should emit a nil value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage == nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createMalformedTabBarRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenWindowIsFireWindow_thenIsNotSetAsRemoteMessage() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: true) + let expectation = XCTestExpectation(description: "Publisher should emit a nil value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage == nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createTabBarRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenTabBarRemoteMessageIsCorrect_thenIsSet() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: false) + let expectation = XCTestExpectation(description: "Publisher should not emit a value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage != nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createTabBarRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + // MARK: - Utilities + + private func createTabBarRemoteMessage() -> RemoteMessageModel { + let tabBarRemoteMessageContent: RemoteMessageModelType = .bigSingleAction(titleText: "Help Us Improve", + descriptionText: "We really want to know which features would make our browser better.", + placeholder: .announce, + primaryActionText: "Tell Us What You Think", + primaryAction: .survey(value: "www.survey.com")) + return RemoteMessageModel(id: TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId, + content: tabBarRemoteMessageContent, + matchingRules: [Int](), + exclusionRules: [Int](), + isMetricsEnabled: true) + } + + private func createMalformedTabBarRemoteMessage() -> RemoteMessageModel { + let tabBarRemoteMessageContent: RemoteMessageModelType = .bigSingleAction(titleText: "Help Us Improve", + descriptionText: "We really want to know which features would make our browser better.", + placeholder: .announce, + primaryActionText: "Tell Us What You Think", + primaryAction: .appStore) + return RemoteMessageModel(id: TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId, + content: tabBarRemoteMessageContent, + matchingRules: [Int](), + exclusionRules: [Int](), + isMetricsEnabled: true) + } + + private func createOtherRemoteMessage() -> RemoteMessageModel { + let tabBarRemoteMessageContent: RemoteMessageModelType = .bigSingleAction(titleText: "Some title!", + descriptionText: "Some description", + placeholder: .announce, + primaryActionText: "Primary!", + primaryAction: .survey(value: "www.survey.com")) + return RemoteMessageModel(id: "other_id", + content: tabBarRemoteMessageContent, + matchingRules: [Int](), + exclusionRules: [Int](), + isMetricsEnabled: true) + } +} + +class MockTabBarRemoteMessageProvider: TabBarRemoteMessageProviding { + private let remoteMessageSubject = PassthroughSubject() + + var remoteMessagePublisher: AnyPublisher { + return remoteMessageSubject.eraseToAnyPublisher() + } + + func emitRemoteMessage(_ message: RemoteMessageModel?) { + remoteMessageSubject.send(message) + } + + func markRemoteMessageAsShown() async { + // No-op + } + + func onSurveyOpened() async { + // No-op + } + + func onMessageDismissed() async { + // No-op + } +}