diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 50887e402a..de5f4688b1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2872,6 +2872,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 */; }; @@ -2880,6 +2882,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 */; }; @@ -4848,12 +4860,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 = ""; }; @@ -5778,6 +5796,8 @@ 3712091F2C232E2B003ADF3D /* RemoteMessaging */ = { isa = PBXGroup; children = ( + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */, + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */, 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */, 3768D8432C2CC884004120AE /* RemoteMessagingConfigMatcherProvider.swift */, 3768D83F2C29C1F1004120AE /* ActiveRemoteMessageModel.swift */, @@ -8322,6 +8342,7 @@ AA86491224D831A1001BABEE /* View */ = { isa = PBXGroup; children = ( + BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresenting.swift */, AA80EC7B256C46AA007083E7 /* TabBar.storyboard */, 1430DFF424D0580F00B8978C /* TabBarViewController.swift */, 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */, @@ -8459,6 +8480,7 @@ AA8EDF1F2491FCC10071C2E8 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */, 37D23779287EB8CA00BCE03B /* TabIndex.swift */, AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */, ); @@ -8560,6 +8582,7 @@ AA9FF95724A1ECE20039E328 /* Model */ = { isa = PBXGroup; children = ( + BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */, AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */, ); path = Model; @@ -8772,6 +8795,7 @@ AAC9C01A24CB592E00AD1325 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */, AAC9C01D24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift */, 37D23788288009CF00BCE03B /* TabCollectionViewModelTests+PinnedTabs.swift */, 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */, @@ -11313,6 +11337,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 */, @@ -11334,6 +11359,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 */, @@ -11702,6 +11728,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 */, @@ -11990,6 +12017,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 */, @@ -12139,6 +12167,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 */, @@ -12340,6 +12369,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 */, @@ -12991,6 +13021,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 */, @@ -13136,6 +13167,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 */, @@ -13173,6 +13205,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 */, @@ -13597,6 +13630,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 */, @@ -13785,6 +13819,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 */, @@ -14082,6 +14117,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 + } +}