From 3283e6e7490c97fe0c143f8f9f9b3fb877230d4e Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 27 Nov 2024 17:37:32 -0300 Subject: [PATCH 01/27] Implement tab bar remote message --- DuckDuckGo.xcodeproj/project.pbxproj | 20 +++ .../dax-response.imageset/Contents.json | 12 ++ .../Response-DDG-Question-96x96.svg | 20 +++ .../Common/Extensions/URLExtension.swift | 4 + DuckDuckGo/HomePage/View/HomePageView.swift | 5 +- .../MainWindow/MainViewController.swift | 2 +- .../ActiveRemoteMessageModel+NewTabPage.swift | 5 +- .../ActiveRemoteMessageModel.swift | 7 + .../RemoteMessagingClient.swift | 2 +- .../TabBarRemoteMessageView.swift | 163 ++++++++++++++++++ .../TabBarRemoteMessageViewModel.swift | 98 +++++++++++ .../TabBar/View/TabBarViewController.swift | 76 +++++++- 12 files changed, 407 insertions(+), 7 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/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift create mode 100644 DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 50887e402a..c52c874556 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,8 @@ 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 */; }; + 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 +4852,14 @@ 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 = ""; }; + 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 = ""; }; @@ -8312,6 +8318,7 @@ AA86491124D8318F001BABEE /* TabBar */ = { isa = PBXGroup; children = ( + BB68B6542D0B1E9200CEC812 /* TabBarRemoteMessaging */, AA86491224D831A1001BABEE /* View */, AA8EDF1F2491FCC10071C2E8 /* ViewModel */, AA9FF95724A1ECE20039E328 /* Model */, @@ -9513,6 +9520,15 @@ path = View; sourceTree = ""; }; + BB68B6542D0B1E9200CEC812 /* TabBarRemoteMessaging */ = { + isa = PBXGroup; + children = ( + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */, + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */, + ); + path = TabBarRemoteMessaging; + sourceTree = ""; + }; BD7090D42C540C0D009EED82 /* MetadataCollectors */ = { isa = PBXGroup; children = ( @@ -11702,6 +11718,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 +12007,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 */, @@ -13173,6 +13191,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 */, @@ -13785,6 +13804,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 */, 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/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index f1c403f08a..0b8662a95b 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -371,6 +371,10 @@ extension URL { return URL(string: "https://duckduckgo.com/updates")! } + static var survey: URL { + return URL(string: "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2")! + } + static var webTrackingProtection: URL { return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/")! } diff --git a/DuckDuckGo/HomePage/View/HomePageView.swift b/DuckDuckGo/HomePage/View/HomePageView.swift index 44c71a6288..8fe1f5ff25 100644 --- a/DuckDuckGo/HomePage/View/HomePageView.swift +++ b/DuckDuckGo/HomePage/View/HomePageView.swift @@ -183,7 +183,10 @@ extension HomePage.Views { @ViewBuilder func remoteMessage() -> some View { - if let remoteMessage = activeRemoteMessageModel.remoteMessage, let modelType = remoteMessage.content, modelType.isSupported { + if let remoteMessage = activeRemoteMessageModel.remoteMessage, + !remoteMessage.isForTabBar, + 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..c6d7c7b8fb 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -22,7 +22,10 @@ import RemoteMessaging extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { var remoteMessagePublisher: AnyPublisher { - $remoteMessage.dropFirst().eraseToAnyPublisher() + $remoteMessage + .dropFirst() + .filter { $0?.isForTabBar == true } + .eraseToAnyPublisher() } func isMessageSupported(_ message: RemoteMessageModel) -> Bool { diff --git a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift index 0af0f67f97..bf933c1774 100644 --- a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift +++ b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift @@ -185,3 +185,10 @@ extension RemoteMessageModelType { } } } + +extension RemoteMessageModel { + + var isForTabBar: Bool { + return id == TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId + } +} diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 3289912570..265658a8a0 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,7 +47,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! #endif diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift new file mode 100644 index 0000000000..3f7d240f57 --- /dev/null +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -0,0 +1,163 @@ +// +// 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 presentPopup: Bool = false + + let model: TabBarRemoteMessage + let onClose: () -> Void + let onTap: (URL) -> Void + let onHover: () -> Void + + var body: some View { + HStack { + Button(model.buttonTitle) { + onTap(model.surveyURL) + } + .buttonStyle(DefaultActionButtonStyle( + enabled: true, + onClose: { onClose() }, + onHoverStart: { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + presentPopup = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + presentPopup = false + } + } + + onHover() + }, + onHoverEnd: { presentPopup = false }) + ) + .frame(width: 147, height: 24) + .popover(isPresented: $presentPopup, arrowEdge: .bottom) { + HStack(alignment: .center) { + Image(.daxResponse) + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + .padding(.leading, 12) + + VStack(alignment: .leading) { + Text(model.popupTitle) + .font(.body) + .fontWeight(.bold) + .frame(alignment: .leading) + .padding(.bottom, 12) + + Text(model.popupSubtitle) + .font(.body) + .fontWeight(.medium) + .frame(alignment: .leading) + + } + .frame(maxWidth: 360, minHeight: 90) + .padding(.trailing, 24) + .padding(.leading, 4) + } + } + } + } +} + +private struct DefaultActionButtonStyle: ButtonStyle { + + public let enabled: Bool + public let onClose: () -> Void + public let onHoverStart: () -> Void + public let onHoverEnd: () -> Void + + public init( + enabled: Bool, + onClose: @escaping () -> Void, + onHoverStart: @escaping () -> Void = {}, + onHoverEnd: @escaping () -> Void = {} + ) { + self.enabled = enabled + self.onClose = onClose + self.onHoverStart = onHoverStart + self.onHoverEnd = onHoverEnd + } + + public func makeBody(configuration: Self.Configuration) -> some View { + ButtonContent( + configuration: configuration, + enabled: enabled, + onClose: onClose, + onHoverStart: onHoverStart, + onHoverEnd: onHoverEnd + ) + } + + struct ButtonContent: View { + let configuration: Configuration + let enabled: Bool + let onClose: () -> Void + let onHoverStart: () -> Void + let onHoverEnd: () -> Void + + @State private var isHovered: Bool = false + + var body: some View { + let enabledBackgroundColor = configuration.isPressed + ? Color("PrimaryButtonPressed") + : (isHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + + let disabledBackgroundColor = Color.gray.opacity(0.1) + let enabledLabelColor = configuration.isPressed ? Color.white.opacity(0.8) : Color.white + let disabledLabelColor = Color.primary.opacity(0.3) + + HStack(spacing: 5) { + configuration.label + .font(.system(size: 13)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Button(action: { + onClose() + }) { + Image(.close) + .resizable() + .frame(width: 12, height: 12) + .foregroundColor(enabled ? Color.white : Color.primary.opacity(0.3)) + } + .buttonStyle(PlainButtonStyle()) // Avoids additional styling + } + .frame(minWidth: 44) + .padding(.top, 2.5) + .padding(.bottom, 3) + .padding(.horizontal, 7.5) + .background(enabled ? enabledBackgroundColor : disabledBackgroundColor) + .foregroundColor(enabled ? enabledLabelColor : disabledLabelColor) + .cornerRadius(5) + .onHover { hovering in + isHovered = hovering + if hovering { + onHoverStart() + } else { + onHoverEnd() + } + } + } + } +} diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift new file mode 100644 index 0000000000..9547215b1c --- /dev/null +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift @@ -0,0 +1,98 @@ +// +// 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 + +struct TabBarRemoteMessage { + static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar" + + let buttonTitle: String + let popupTitle: String + let popupSubtitle: String + let surveyURL: URL +} + +final class TabBarRemoteMessageViewModel: ObservableObject { + + private let activeRemoteMessageModel: ActiveRemoteMessageModel + private var cancellable: AnyCancellable? + + @Published var remoteMessage: TabBarRemoteMessage? + + init(activeRemoteMessageModel: ActiveRemoteMessageModel) { + self.activeRemoteMessageModel = activeRemoteMessageModel + + cancellable = activeRemoteMessageModel.$remoteMessage + .sink(receiveValue: { model in + guard let model = model else { + self.remoteMessage = nil + return + } + + if model.shouldShowTabBarRemoteMessage, let tabBarRemoteMessage = model.mapToTabBarRemoteMessage() { + self.remoteMessage = tabBarRemoteMessage + } + }) + } + + func onDismiss() { + Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .close) } + } + + /// When the user hovers the Tab Bar Remote Message and we show the popup, there is where when we mark + /// that the user really saw the message. + func onUserHovered() { + Task { await activeRemoteMessageModel.markRemoteMessageAsShown() } + } + + func onOpenSurvey() { + Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction) } + } +} + +private extension RemoteMessageModel { + + var shouldShowTabBarRemoteMessage: Bool { + guard let modelType = content else { return false } + + return modelType.isSupported && isForTabBar + } + + 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/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index f9038f0b94..d10e67df2d 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -23,6 +23,7 @@ import Lottie import SwiftUI import WebKit import os.log +import RemoteMessaging final class TabBarViewController: NSViewController { @@ -70,6 +71,9 @@ final class TabBarViewController: NSViewController { private let pinnedTabsViewModel: PinnedTabsViewModel? private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? + private let tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel + private let feedbackPopoverViewController: PopoverMessageViewController + private var feedbackBarButtonHostingController: NSHostingController? private var selectionIndexCancellable: AnyCancellable? private var mouseDownCancellable: AnyCancellable? @@ -86,9 +90,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 +100,9 @@ final class TabBarViewController: NSViewController { fatalError("TabBarViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel) { + init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) { self.tabCollectionViewModel = tabCollectionViewModel + self.tabBarRemoteMessageViewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: activeRemoteMessageModel) if !tabCollectionViewModel.isBurner, let pinnedTabCollection = tabCollectionViewModel.pinnedTabsManager?.tabCollection { let pinnedTabsViewModel = PinnedTabsViewModel(collection: pinnedTabCollection) let pinnedTabsView = PinnedTabsView(model: pinnedTabsViewModel) @@ -110,6 +115,15 @@ final class TabBarViewController: NSViewController { self.pinnedTabsHostingView = nil } + feedbackPopoverViewController = PopoverMessageViewController( + title: "Tell Us What You Think", + message: "Take our short survey and help us build the best browser.", + image: .daxResponse, + shouldShowCloseButton: false, + presentMultiline: true, + autoDismissDuration: nil + ) + super.init(coder: coder) } @@ -124,6 +138,7 @@ final class TabBarViewController: NSViewController { subscribeToTabModeChanges() setupAddTabButton() setupAsBurnerWindowIfNeeded() + addTabBarRemoteMessageListener() } override func viewWillAppear() { @@ -200,6 +215,61 @@ final class TabBarViewController: NSViewController { } } + private func addTabBarRemoteMessageListener() { + tabBarRemoteMessageViewModel.$remoteMessage.sink(receiveValue: { tabBarRemoteMessage in + if let tabBarRemoteMessage = tabBarRemoteMessage { + if self.feedbackBarButtonHostingController == nil { + self.showTabBarRemoteMessage(tabBarRemoteMessage) + } + } else { + if self.feedbackBarButtonHostingController != nil { + self.removeFeedbackButton() + } + } + }) + .store(in: &cancellables) + } + + private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { + let feedbackButtonView = TabBarRemoteMessageView( + model: tabBarRemotMessage, + onClose: { + self.tabBarRemoteMessageViewModel.onDismiss() + self.removeFeedbackButton() + }, + onTap: { surveyURL in + WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) + self.tabBarRemoteMessageViewModel.onOpenSurvey() + self.removeFeedbackButton() + }, + onHover: { + self.tabBarRemoteMessageViewModel.onUserHovered() + } + ) + 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.heightAnchor.constraint(equalToConstant: 24), + feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) + ]) + } + + private func removeFeedbackButton() { + guard let hostingController = feedbackBarButtonHostingController else { return } + + rightSideStackView.removeArrangedSubview(hostingController.view) + hostingController.view.removeFromSuperview() + hostingController.removeFromParent() + feedbackBarButtonHostingController = nil + } + private func setupPinnedTabsView() { layoutPinnedTabsView() subscribeToPinnedTabsViewModelOutputs() From 2a880c06e2b0d31a3bcab6be6ed562d4dfdad2de Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 18:05:38 -0300 Subject: [PATCH 02/27] Change production URL --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 265658a8a0..142a1238cb 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -49,7 +49,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { #if DEBUG URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else - URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #endif }() } From 62c842622dda93d5b013be076bf1d09bc7ee3de1 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 18:35:25 -0300 Subject: [PATCH 03/27] Add JSON to be used as debug --- tab-bar-remote-json-endpoint.json | 231 ++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 tab-bar-remote-json-endpoint.json diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json new file mode 100644 index 0000000000..2493aaf771 --- /dev/null +++ b/tab-bar-remote-json-endpoint.json @@ -0,0 +1,231 @@ +{ + "version": 9, + "messages": [ + { + "id": "macos_permanent_survey_tab_bar", + "content": { + "messageType": "big_single_action", + "titleText": "Help Us Improve", + "descriptionText": "Take our short survey and help us build the best browser.", + "placeholder": "Announce", + "primaryActionText": "Share Your Thoughts", + "primaryAction": { + "type": "survey", + "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", + "additionalParameters": { + "queryParams": "delta;var;osv;ddgv" + } + } + }, + "matchingRules": [] + } + ], + "rules": [ + { + "id": 1, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproPurchasePlatform": { + "value": [ + "apple" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "expiring" + ] + }, + "pproDaysUntilExpiryOrRenewal": { + "max": 10 + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": true + } + } + }, + { + "id": 2, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproPurchasePlatform": { + "value": [ + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "expiring" + ] + }, + "pproDaysUntilExpiryOrRenewal": { + "max": 10 + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 3, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproDaysSinceSubscribed": { + "min": 14 + }, + "pproPurchasePlatform": { + "value": [ + "apple", + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": true + } + } + }, + { + "id": 4, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproDaysSinceSubscribed": { + "min": 14 + }, + "pproPurchasePlatform": { + "value": [ + "apple", + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 5, + "attributes": { + "interactedWithMessage": { + "value": [ + "macos_privacy_pro_exit_survey_1", + "macos_privacy_pro_sparkle_exit_survey_1", + "macos_privacy_pro_app_store_exit_survey_1" + ] + } + } + }, + { + "id": 6, + "attributes": { + "interactedWithMessage": { + "value": [ + "macos_privacy_pro_subscriber_survey_1" + ] + } + } + }, + { + "id": 7, + "attributes": { + "interactedWithDeprecatedMacRemoteMessage": { + "value": [ + "privacy_pro_exit_survey_1" + ] + } + } + }, + { + "id": 8, + "attributes": { + "interactedWithDeprecatedMacRemoteMessage": { + "value": [ + "privacy_pro_survey_1" + ] + } + } + }, + { + "id": 9, + "attributes": { + "appVersion": { + "min": "1.106.0" + }, + "daysSinceInstalled": { + "min": 14, + "max": 21 + }, + "locale": { + "value": [ + "en-US", + "en-CA", + "en-GB", + "en-AU" + ] + } + } + }, + { + "id": 10, + "attributes": { + "appVersion": { + "min": "1.99.0", + "max": "1.102.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 11, + "attributes": { + "appVersion": { + "min": "1.116.0.0", + "max": "1.116.0.999999" + }, + "locale": { + "value": [ + "en-US" + ] + }, + "pproSubscriber": { + "value": true + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + } + } + } + ] +} From aa916e3403a2feee8106049c37d712ac270cab49 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 18:39:07 -0300 Subject: [PATCH 04/27] Use raw JSON --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 142a1238cb..91ea2b7288 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,9 +47,9 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! #else - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! #endif }() } From 3f07a6d924ef97c0377a79a04af287fcf6f5d510 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 19:31:37 -0300 Subject: [PATCH 05/27] Make button as tall as the tab bar --- .../TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index 3f7d240f57..27341fba65 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -47,7 +47,7 @@ struct TabBarRemoteMessageView: View { }, onHoverEnd: { presentPopup = false }) ) - .frame(width: 147, height: 24) + .frame(width: 147) .popover(isPresented: $presentPopup, arrowEdge: .bottom) { HStack(alignment: .center) { Image(.daxResponse) From 2cf6e71a925928b8528bf68b96550b269e879161 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 13 Dec 2024 09:47:27 -0300 Subject: [PATCH 06/27] Minor feedback --- .../RemoteMessagingClient.swift | 2 +- .../TabBarRemoteMessageView.swift | 87 ++++++++++--------- .../TabBar/View/TabBarViewController.swift | 1 - tab-bar-remote-json-endpoint.json | 6 +- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 91ea2b7288..06d5b1ac05 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,7 +47,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! #endif diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index 27341fba65..d6495c8bc0 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -20,6 +20,7 @@ import SwiftUI struct TabBarRemoteMessageView: View { @State private var presentPopup: Bool = false + @State private var hoverTimer: Timer? let model: TabBarRemoteMessage let onClose: () -> Void @@ -35,45 +36,55 @@ struct TabBarRemoteMessageView: View { enabled: true, onClose: { onClose() }, onHoverStart: { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - presentPopup = true - - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - presentPopup = false - } - } - + startHoverTimer() onHover() }, - onHoverEnd: { presentPopup = false }) + onHoverEnd: { + cancelHoverTimer() + }) ) .frame(width: 147) .popover(isPresented: $presentPopup, arrowEdge: .bottom) { - HStack(alignment: .center) { - Image(.daxResponse) - .resizable() - .scaledToFit() - .frame(width: 72, height: 72) - .padding(.leading, 12) - - VStack(alignment: .leading) { - Text(model.popupTitle) - .font(.body) - .fontWeight(.bold) - .frame(alignment: .leading) - .padding(.bottom, 12) - - Text(model.popupSubtitle) - .font(.body) - .fontWeight(.medium) - .frame(alignment: .leading) - - } - .frame(maxWidth: 360, minHeight: 90) - .padding(.trailing, 24) - .padding(.leading, 4) - } + PopoverContent(model: model) + } + } + } + + private func startHoverTimer() { + hoverTimer?.invalidate() + hoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + presentPopup = true + } + } + + private func cancelHoverTimer() { + hoverTimer?.invalidate() + presentPopup = false + } +} + +struct PopoverContent: View { + let model: TabBarRemoteMessage + + var body: some View { + HStack(alignment: .center) { + Image(.daxResponse) + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + .padding(.leading, 12) + + VStack(alignment: .leading) { + Text(model.popupTitle) + .font(.system(size: 13, weight: .bold)) + .padding(.bottom, 8) + + Text(model.popupSubtitle) + .font(.system(size: 13, weight: .regular)) } + .frame(width: 360, height: 92) + .padding(.trailing, 24) + .padding(.leading, 4) } } } @@ -133,15 +144,11 @@ private struct DefaultActionButtonStyle: ButtonStyle { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) - Button(action: { - onClose() - }) { + Button(action: { onClose() }) { Image(.close) - .resizable() - .frame(width: 12, height: 12) - .foregroundColor(enabled ? Color.white : Color.primary.opacity(0.3)) } - .buttonStyle(PlainButtonStyle()) // Avoids additional styling + .frame(width: 16, height: 16) + .buttonStyle(PlainButtonStyle()) } .frame(minWidth: 44) .padding(.top, 2.5) diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index d10e67df2d..1beb645786 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -256,7 +256,6 @@ final class TabBarViewController: NSViewController { rightSideStackView.insertArrangedSubview(feedbackBarButtonHostingController.view, at: index) NSLayoutConstraint.activate([ - feedbackBarButtonHostingController.view.heightAnchor.constraint(equalToConstant: 24), feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) ]) } diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json index 2493aaf771..c9ab320017 100644 --- a/tab-bar-remote-json-endpoint.json +++ b/tab-bar-remote-json-endpoint.json @@ -1,4 +1,4 @@ -{ +i{ "version": 9, "messages": [ { @@ -6,9 +6,9 @@ "content": { "messageType": "big_single_action", "titleText": "Help Us Improve", - "descriptionText": "Take our short survey and help us build the best browser.", + "descriptionText": "We really want to know which features would make our browser better.", "placeholder": "Announce", - "primaryActionText": "Share Your Thoughts", + "primaryActionText": "Tell Us What You Think", "primaryAction": { "type": "survey", "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", From 2b745427bd5fbb578526d707c5de5355e32f9867 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 15 Dec 2024 19:52:57 -0300 Subject: [PATCH 07/27] More improvements --- .../TabBarRemoteMessageView.swift | 40 ++++++-------- .../TabBar/View/TabBarViewController.swift | 54 +++++++++++++++---- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index d6495c8bc0..3bf4c5b6b4 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -20,12 +20,12 @@ import SwiftUI struct TabBarRemoteMessageView: View { @State private var presentPopup: Bool = false - @State private var hoverTimer: Timer? let model: TabBarRemoteMessage let onClose: () -> Void let onTap: (URL) -> Void let onHover: () -> Void + let onHoverEnd: () -> Void var body: some View { HStack { @@ -36,56 +36,46 @@ struct TabBarRemoteMessageView: View { enabled: true, onClose: { onClose() }, onHoverStart: { - startHoverTimer() onHover() }, onHoverEnd: { - cancelHoverTimer() + onHoverEnd() }) ) .frame(width: 147) - .popover(isPresented: $presentPopup, arrowEdge: .bottom) { - PopoverContent(model: model) - } - } - } - - private func startHoverTimer() { - hoverTimer?.invalidate() - hoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in - presentPopup = true } } +} - private func cancelHoverTimer() { - hoverTimer?.invalidate() - presentPopup = false +struct TabBarRemoteMessagePopoverContent: View { + enum Constants { + static let height: CGFloat = 92 + static let width: CGFloat = 360 } -} -struct PopoverContent: View { let model: TabBarRemoteMessage var body: some View { - HStack(alignment: .center) { + HStack(alignment: .center, spacing: 0) { Image(.daxResponse) .resizable() .scaledToFit() .frame(width: 72, height: 72) - .padding(.leading, 12) + .padding(.leading, 8) + .padding(.trailing, 16) - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { Text(model.popupTitle) .font(.system(size: 13, weight: .bold)) .padding(.bottom, 8) Text(model.popupSubtitle) - .font(.system(size: 13, weight: .regular)) + .font(.system(size: 13, weight: .medium)) } - .frame(width: 360, height: 92) - .padding(.trailing, 24) - .padding(.leading, 4) + .padding(.trailing, 12) + .padding([.bottom, .top], 10) } + .frame(width: Constants.width, height: Constants.height) } } diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 1beb645786..0bdd871fb7 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -72,7 +72,8 @@ final class TabBarViewController: NSViewController { private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? private let tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel - private let feedbackPopoverViewController: PopoverMessageViewController + private var tabBarRemoteMessagePopover: NSPopover? + private var tabBarRemoteMessagePopoverHoverTimer: Timer? private var feedbackBarButtonHostingController: NSHostingController? private var selectionIndexCancellable: AnyCancellable? @@ -115,15 +116,6 @@ final class TabBarViewController: NSViewController { self.pinnedTabsHostingView = nil } - feedbackPopoverViewController = PopoverMessageViewController( - title: "Tell Us What You Think", - message: "Take our short survey and help us build the best browser.", - image: .daxResponse, - shouldShowCloseButton: false, - presentMultiline: true, - autoDismissDuration: nil - ) - super.init(coder: coder) } @@ -243,7 +235,11 @@ final class TabBarViewController: NSViewController { self.removeFeedbackButton() }, onHover: { + self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) self.tabBarRemoteMessageViewModel.onUserHovered() + }, + onHoverEnd: { + self.dismissTabBarRemoteMessagePopover() } ) feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) @@ -260,6 +256,44 @@ final class TabBarViewController: NSViewController { ]) } + private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + self.showTabBarRemotePopup(message) + } + } + + private func dismissTabBarRemoteMessagePopover() { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopover?.close() + } + + private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { + if let popover = tabBarRemoteMessagePopover { + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + + popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } else { + tabBarRemoteMessagePopover = NSPopover() + tabBarRemoteMessagePopover?.animates = true + tabBarRemoteMessagePopover?.behavior = .semitransient + tabBarRemoteMessagePopover?.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, + height: TabBarRemoteMessagePopoverContent.Constants.height) + + let controller = NSViewController() + controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + tabBarRemoteMessagePopover?.contentViewController = controller + + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + + tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } + } + private func removeFeedbackButton() { guard let hostingController = feedbackBarButtonHostingController else { return } From ecc4860c0f361bffa9dcfe830532fae18a9b12d3 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 15 Dec 2024 20:11:21 -0300 Subject: [PATCH 08/27] Update JSON link --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 06d5b1ac05..e7b3a6e619 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -49,7 +49,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { #if DEBUG URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else - URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/refs/heads/juan/poc-new-user-feedfack-point-of-action/tab-bar-remote-json-endpoint.json")! #endif }() } From 3dadbce38d6a700dcad5ed00a11266eb4027f1b4 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 15 Dec 2024 20:32:26 -0300 Subject: [PATCH 09/27] Fix JSON --- tab-bar-remote-json-endpoint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json index c9ab320017..4ab0822362 100644 --- a/tab-bar-remote-json-endpoint.json +++ b/tab-bar-remote-json-endpoint.json @@ -1,4 +1,4 @@ -i{ +{ "version": 9, "messages": [ { From 0499ff2be915b7a559f781996e6f7abd39f45f2b Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 11:08:15 -0300 Subject: [PATCH 10/27] Add unit tests --- DuckDuckGo.xcodeproj/project.pbxproj | 18 ++ .../Model/TabBarActiveRemoteMessage.swift | 52 ++++++ .../TabBar/Model/TabBarRemoteMessage.swift | 26 +++ .../TabBarRemoteMessageView.swift | 2 + .../TabBarRemoteMessageViewModel.swift | 33 ++-- .../TabBar/View/TabBarViewController.swift | 48 +++--- .../TabBarRemoteMessageViewModelTests.swift | 159 ++++++++++++++++++ 7 files changed, 296 insertions(+), 42 deletions(-) create mode 100644 DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift create mode 100644 DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift create mode 100644 UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c52c874556..b0cdfd5047 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2882,6 +2882,12 @@ 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 */; }; + 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 */; }; @@ -4859,6 +4865,9 @@ 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 = ""; }; + 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 = ""; }; @@ -8567,6 +8576,8 @@ AA9FF95724A1ECE20039E328 /* Model */ = { isa = PBXGroup; children = ( + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */, + BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */, AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */, ); path = Model; @@ -8779,6 +8790,7 @@ AAC9C01A24CB592E00AD1325 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */, AAC9C01D24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift */, 37D23788288009CF00BCE03B /* TabCollectionViewModelTests+PinnedTabs.swift */, 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */, @@ -11329,6 +11341,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 */, @@ -12157,6 +12170,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 */, @@ -12358,6 +12372,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 */, @@ -13009,6 +13024,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 */, @@ -13616,6 +13632,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 */, @@ -14102,6 +14119,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/TabBar/Model/TabBarActiveRemoteMessage.swift b/DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift new file mode 100644 index 0000000000..9ee658c0d0 --- /dev/null +++ b/DuckDuckGo/TabBar/Model/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.$remoteMessage.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/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/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index 3bf4c5b6b4..34d638a76b 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -26,6 +26,7 @@ struct TabBarRemoteMessageView: View { let onTap: (URL) -> Void let onHover: () -> Void let onHoverEnd: () -> Void + let onAppear: () -> Void var body: some View { HStack { @@ -42,6 +43,7 @@ struct TabBarRemoteMessageView: View { onHoverEnd() }) ) + .onAppear(perform: { onAppear() }) .frame(width: 147) } } diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift index 9547215b1c..0a8f3c3eff 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift @@ -19,27 +19,20 @@ import Combine import RemoteMessaging -struct TabBarRemoteMessage { - static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar" - - let buttonTitle: String - let popupTitle: String - let popupSubtitle: String - let surveyURL: URL -} - final class TabBarRemoteMessageViewModel: ObservableObject { - private let activeRemoteMessageModel: ActiveRemoteMessageModel + private let tabBarRemoteActiveMessage: TabBarRemoteMessageProviding private var cancellable: AnyCancellable? @Published var remoteMessage: TabBarRemoteMessage? - init(activeRemoteMessageModel: ActiveRemoteMessageModel) { - self.activeRemoteMessageModel = activeRemoteMessageModel + init(activeRemoteMessageModel: TabBarRemoteMessageProviding, isFireWindow: Bool) { + self.tabBarRemoteActiveMessage = activeRemoteMessageModel - cancellable = activeRemoteMessageModel.$remoteMessage + cancellable = tabBarRemoteActiveMessage.remoteMessagePublisher .sink(receiveValue: { model in + guard !isFireWindow else { return } + guard let model = model else { self.remoteMessage = nil return @@ -51,18 +44,16 @@ final class TabBarRemoteMessageViewModel: ObservableObject { }) } - func onDismiss() { - Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .close) } + func onSurveyOpened() { + Task { await tabBarRemoteActiveMessage.onSurveyOpened() } } - /// When the user hovers the Tab Bar Remote Message and we show the popup, there is where when we mark - /// that the user really saw the message. - func onUserHovered() { - Task { await activeRemoteMessageModel.markRemoteMessageAsShown() } + func onMessageDismissed() { + Task { await tabBarRemoteActiveMessage.onMessageDismissed() } } - func onOpenSurvey() { - Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction) } + func markTabBarRemoteMessageAsShown() { + Task { await tabBarRemoteActiveMessage.markRemoteMessageAsShown() } } } diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 0bdd871fb7..37aafe6888 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -103,7 +103,9 @@ final class TabBarViewController: NSViewController { init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) { self.tabCollectionViewModel = tabCollectionViewModel - self.tabBarRemoteMessageViewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: activeRemoteMessageModel) + 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) @@ -226,20 +228,22 @@ final class TabBarViewController: NSViewController { let feedbackButtonView = TabBarRemoteMessageView( model: tabBarRemotMessage, onClose: { - self.tabBarRemoteMessageViewModel.onDismiss() + self.tabBarRemoteMessageViewModel.onMessageDismissed() self.removeFeedbackButton() }, onTap: { surveyURL in WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) - self.tabBarRemoteMessageViewModel.onOpenSurvey() + self.tabBarRemoteMessageViewModel.onSurveyOpened() self.removeFeedbackButton() }, onHover: { self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) - self.tabBarRemoteMessageViewModel.onUserHovered() }, onHoverEnd: { self.dismissTabBarRemoteMessagePopover() + }, + onAppear: { + self.tabBarRemoteMessageViewModel.markTabBarRemoteMessageAsShown() } ) feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) @@ -258,7 +262,7 @@ final class TabBarViewController: NSViewController { private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in self.showTabBarRemotePopup(message) } } @@ -269,31 +273,33 @@ final class TabBarViewController: NSViewController { } private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { - if let popover = tabBarRemoteMessagePopover { - guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { - return - } + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + if let popover = tabBarRemoteMessagePopover { popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) } else { tabBarRemoteMessagePopover = NSPopover() - tabBarRemoteMessagePopover?.animates = true - tabBarRemoteMessagePopover?.behavior = .semitransient - tabBarRemoteMessagePopover?.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, - height: TabBarRemoteMessagePopoverContent.Constants.height) - - let controller = NSViewController() - controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) - tabBarRemoteMessagePopover?.contentViewController = controller - - guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { - return - } + configurePopover(with: message) tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) } } + private func configurePopover(with message: TabBarRemoteMessage) { + guard let popover = tabBarRemoteMessagePopover else { return } + + popover.animates = true + popover.behavior = .semitransient + popover.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, + height: TabBarRemoteMessagePopoverContent.Constants.height) + + let controller = NSViewController() + controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + popover.contentViewController = controller + } + private func removeFeedbackButton() { guard let hostingController = feedbackBarButtonHostingController else { return } 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 + } +} From 472e37ec8ffd384fc417e7b7aaca08886c88564b Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 11:10:00 -0300 Subject: [PATCH 11/27] Revert JSON changes --- .../RemoteMessagingClient.swift | 4 +- tab-bar-remote-json-endpoint.json | 231 ------------------ 2 files changed, 2 insertions(+), 233 deletions(-) delete mode 100644 tab-bar-remote-json-endpoint.json diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index e7b3a6e619..3289912570 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,9 +47,9 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! #else - URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/refs/heads/juan/poc-new-user-feedfack-point-of-action/tab-bar-remote-json-endpoint.json")! + URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! #endif }() } diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json deleted file mode 100644 index 4ab0822362..0000000000 --- a/tab-bar-remote-json-endpoint.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "version": 9, - "messages": [ - { - "id": "macos_permanent_survey_tab_bar", - "content": { - "messageType": "big_single_action", - "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": { - "type": "survey", - "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", - "additionalParameters": { - "queryParams": "delta;var;osv;ddgv" - } - } - }, - "matchingRules": [] - } - ], - "rules": [ - { - "id": 1, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproPurchasePlatform": { - "value": [ - "apple" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "expiring" - ] - }, - "pproDaysUntilExpiryOrRenewal": { - "max": 10 - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": true - } - } - }, - { - "id": 2, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproPurchasePlatform": { - "value": [ - "stripe" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "expiring" - ] - }, - "pproDaysUntilExpiryOrRenewal": { - "max": 10 - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": false - } - } - }, - { - "id": 3, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproDaysSinceSubscribed": { - "min": 14 - }, - "pproPurchasePlatform": { - "value": [ - "apple", - "stripe" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "active" - ] - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": true - } - } - }, - { - "id": 4, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproDaysSinceSubscribed": { - "min": 14 - }, - "pproPurchasePlatform": { - "value": [ - "apple", - "stripe" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "active" - ] - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": false - } - } - }, - { - "id": 5, - "attributes": { - "interactedWithMessage": { - "value": [ - "macos_privacy_pro_exit_survey_1", - "macos_privacy_pro_sparkle_exit_survey_1", - "macos_privacy_pro_app_store_exit_survey_1" - ] - } - } - }, - { - "id": 6, - "attributes": { - "interactedWithMessage": { - "value": [ - "macos_privacy_pro_subscriber_survey_1" - ] - } - } - }, - { - "id": 7, - "attributes": { - "interactedWithDeprecatedMacRemoteMessage": { - "value": [ - "privacy_pro_exit_survey_1" - ] - } - } - }, - { - "id": 8, - "attributes": { - "interactedWithDeprecatedMacRemoteMessage": { - "value": [ - "privacy_pro_survey_1" - ] - } - } - }, - { - "id": 9, - "attributes": { - "appVersion": { - "min": "1.106.0" - }, - "daysSinceInstalled": { - "min": 14, - "max": 21 - }, - "locale": { - "value": [ - "en-US", - "en-CA", - "en-GB", - "en-AU" - ] - } - } - }, - { - "id": 10, - "attributes": { - "appVersion": { - "min": "1.99.0", - "max": "1.102.0" - }, - "installedMacAppStore": { - "value": false - } - } - }, - { - "id": 11, - "attributes": { - "appVersion": { - "min": "1.116.0.0", - "max": "1.116.0.999999" - }, - "locale": { - "value": [ - "en-US" - ] - }, - "pproSubscriber": { - "value": true - }, - "pproSubscriptionStatus": { - "value": [ - "active" - ] - } - } - } - ] -} From 52a86beed563f65c3d5881e2b3d56e85bb8dd9ea Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 11:14:14 -0300 Subject: [PATCH 12/27] Remove URL extension --- DuckDuckGo/Common/Extensions/URLExtension.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 0b8662a95b..f1c403f08a 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -371,10 +371,6 @@ extension URL { return URL(string: "https://duckduckgo.com/updates")! } - static var survey: URL { - return URL(string: "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2")! - } - static var webTrackingProtection: URL { return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/")! } From 5aca880f2f03f70a5000d03dba0fcaeabfbe46aa Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 12:43:29 -0300 Subject: [PATCH 13/27] Fix deallocation issue --- .../TabBar/View/TabBarViewController.swift | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 37aafe6888..1e3191125a 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -79,6 +79,7 @@ final class TabBarViewController: NSViewController { private var selectionIndexCancellable: AnyCancellable? private var mouseDownCancellable: AnyCancellable? private var cancellables = Set() + private var tabBarRemoteMessageCancellable: AnyCancellable? @IBOutlet weak var shadowView: TabShadowView! @@ -132,7 +133,6 @@ final class TabBarViewController: NSViewController { subscribeToTabModeChanges() setupAddTabButton() setupAsBurnerWindowIfNeeded() - addTabBarRemoteMessageListener() } override func viewWillAppear() { @@ -145,12 +145,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() { @@ -210,18 +212,18 @@ final class TabBarViewController: NSViewController { } private func addTabBarRemoteMessageListener() { - tabBarRemoteMessageViewModel.$remoteMessage.sink(receiveValue: { tabBarRemoteMessage in - if let tabBarRemoteMessage = tabBarRemoteMessage { - if self.feedbackBarButtonHostingController == nil { - self.showTabBarRemoteMessage(tabBarRemoteMessage) + 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() + } } - } else { - if self.feedbackBarButtonHostingController != nil { - self.removeFeedbackButton() - } - } - }) - .store(in: &cancellables) + }) } private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { From ba6eadf55abfa521a19f488dadf9c9790999d325 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 14:37:48 -0300 Subject: [PATCH 14/27] Fix filtering on remote message NTP --- DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift index c6d7c7b8fb..42e621054a 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -24,7 +24,7 @@ extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { var remoteMessagePublisher: AnyPublisher { $remoteMessage .dropFirst() - .filter { $0?.isForTabBar == true } + .filter { $0?.isForTabBar == false } .eraseToAnyPublisher() } From 5e8c3b9d555b994255d000c94cc6895198a0cb91 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 15:22:51 -0300 Subject: [PATCH 15/27] Move models to RemoteMessaging folder --- DuckDuckGo.xcodeproj/project.pbxproj | 14 +++----------- .../TabBarActiveRemoteMessage.swift | 0 .../TabBarRemoteMessageView.swift | 0 .../TabBarRemoteMessageViewModel.swift | 0 4 files changed, 3 insertions(+), 11 deletions(-) rename DuckDuckGo/{TabBar/Model => RemoteMessaging}/TabBarActiveRemoteMessage.swift (100%) rename DuckDuckGo/{TabBar/TabBarRemoteMessaging => RemoteMessaging}/TabBarRemoteMessageView.swift (100%) rename DuckDuckGo/TabBar/{TabBarRemoteMessaging => ViewModel}/TabBarRemoteMessageViewModel.swift (100%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b0cdfd5047..eb09ffd6dd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5793,6 +5793,8 @@ 3712091F2C232E2B003ADF3D /* RemoteMessaging */ = { isa = PBXGroup; children = ( + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */, + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */, 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */, 3768D8432C2CC884004120AE /* RemoteMessagingConfigMatcherProvider.swift */, 3768D83F2C29C1F1004120AE /* ActiveRemoteMessageModel.swift */, @@ -8327,7 +8329,6 @@ AA86491124D8318F001BABEE /* TabBar */ = { isa = PBXGroup; children = ( - BB68B6542D0B1E9200CEC812 /* TabBarRemoteMessaging */, AA86491224D831A1001BABEE /* View */, AA8EDF1F2491FCC10071C2E8 /* ViewModel */, AA9FF95724A1ECE20039E328 /* Model */, @@ -8475,6 +8476,7 @@ AA8EDF1F2491FCC10071C2E8 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */, 37D23779287EB8CA00BCE03B /* TabIndex.swift */, AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */, ); @@ -8576,7 +8578,6 @@ AA9FF95724A1ECE20039E328 /* Model */ = { isa = PBXGroup; children = ( - BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */, BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */, AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */, ); @@ -9532,15 +9533,6 @@ path = View; sourceTree = ""; }; - BB68B6542D0B1E9200CEC812 /* TabBarRemoteMessaging */ = { - isa = PBXGroup; - children = ( - BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */, - BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */, - ); - path = TabBarRemoteMessaging; - sourceTree = ""; - }; BD7090D42C540C0D009EED82 /* MetadataCollectors */ = { isa = PBXGroup; children = ( diff --git a/DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift b/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift similarity index 100% rename from DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift rename to DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift similarity index 100% rename from DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift rename to DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift similarity index 100% rename from DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift rename to DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift From 3fb8ec20678a1c404681835f4b601f49cc2208ed Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 15:46:53 -0300 Subject: [PATCH 16/27] Create TabBarRemoteMessagePresentable --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + .../RemoteMessagingClient.swift | 2 +- .../View/TabBarRemoteMessagePresenting.swift | 135 ++++++++++++++++++ .../TabBar/View/TabBarViewController.swift | 115 ++------------- 4 files changed, 150 insertions(+), 108 deletions(-) create mode 100644 DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index eb09ffd6dd..de5f4688b1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2882,6 +2882,8 @@ 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 */; }; @@ -4865,6 +4867,7 @@ 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 = ""; }; @@ -8339,6 +8342,7 @@ AA86491224D831A1001BABEE /* View */ = { isa = PBXGroup; children = ( + BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresenting.swift */, AA80EC7B256C46AA007083E7 /* TabBar.storyboard */, 1430DFF424D0580F00B8978C /* TabBarViewController.swift */, 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */, @@ -11355,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 */, @@ -13162,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 */, diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 3289912570..265658a8a0 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,7 +47,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! #endif diff --git a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift new file mode 100644 index 0000000000..294bf6f4b9 --- /dev/null +++ b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift @@ -0,0 +1,135 @@ +// +// 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 + +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 { + + 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() + } + } + }) + } + + private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { + let feedbackButtonView = TabBarRemoteMessageView( + model: tabBarRemotMessage, + onClose: { + self.tabBarRemoteMessageViewModel.onMessageDismissed() + self.removeFeedbackButton() + }, + onTap: { surveyURL in + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) + } + self.tabBarRemoteMessageViewModel.onSurveyOpened() + self.removeFeedbackButton() + }, + onHover: { + self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) + }, + onHoverEnd: { + self.dismissTabBarRemoteMessagePopover() + }, + onAppear: { + 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) + ]) + } + + private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in + self.showTabBarRemotePopup(message) + } + } + + private func dismissTabBarRemoteMessagePopover() { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopover?.close() + } + + 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) + } + } + + private func configurePopover(with message: TabBarRemoteMessage) { + guard let popover = tabBarRemoteMessagePopover else { return } + + popover.animates = true + popover.behavior = .semitransient + popover.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, + height: TabBarRemoteMessagePopoverContent.Constants.height) + + let controller = NSViewController() + controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + popover.contentViewController = 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 1e3191125a..766060ddd6 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -25,7 +25,7 @@ import WebKit import os.log import RemoteMessaging -final class TabBarViewController: NSViewController { +final class TabBarViewController: NSViewController, TabBarRemoteMessagePresenting { enum HorizontalSpace: CGFloat { case pinnedTabsScrollViewPadding = 76 @@ -71,15 +71,16 @@ final class TabBarViewController: NSViewController { private let pinnedTabsViewModel: PinnedTabsViewModel? private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? - private let tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel - private var tabBarRemoteMessagePopover: NSPopover? - private var tabBarRemoteMessagePopoverHoverTimer: Timer? - private var feedbackBarButtonHostingController: NSHostingController? - private var selectionIndexCancellable: AnyCancellable? private var mouseDownCancellable: AnyCancellable? private var cancellables = Set() - private var tabBarRemoteMessageCancellable: AnyCancellable? + + // TabBarRemoteMessagePresentable + var tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel + var tabBarRemoteMessagePopover: NSPopover? + var tabBarRemoteMessagePopoverHoverTimer: Timer? + var feedbackBarButtonHostingController: NSHostingController? + var tabBarRemoteMessageCancellable: AnyCancellable? @IBOutlet weak var shadowView: TabShadowView! @@ -211,106 +212,6 @@ final class TabBarViewController: NSViewController { } } - private 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() - } - } - }) - } - - private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { - let feedbackButtonView = TabBarRemoteMessageView( - model: tabBarRemotMessage, - onClose: { - self.tabBarRemoteMessageViewModel.onMessageDismissed() - self.removeFeedbackButton() - }, - onTap: { surveyURL in - WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) - self.tabBarRemoteMessageViewModel.onSurveyOpened() - self.removeFeedbackButton() - }, - onHover: { - self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) - }, - onHoverEnd: { - self.dismissTabBarRemoteMessagePopover() - }, - onAppear: { - 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) - ]) - } - - private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { - tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in - self.showTabBarRemotePopup(message) - } - } - - private func dismissTabBarRemoteMessagePopover() { - tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopover?.close() - } - - 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) - } - } - - private func configurePopover(with message: TabBarRemoteMessage) { - guard let popover = tabBarRemoteMessagePopover else { return } - - popover.animates = true - popover.behavior = .semitransient - popover.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, - height: TabBarRemoteMessagePopoverContent.Constants.height) - - let controller = NSViewController() - controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) - popover.contentViewController = controller - } - - private func removeFeedbackButton() { - guard let hostingController = feedbackBarButtonHostingController else { return } - - rightSideStackView.removeArrangedSubview(hostingController.view) - hostingController.view.removeFromSuperview() - hostingController.removeFromParent() - feedbackBarButtonHostingController = nil - } - private func setupPinnedTabsView() { layoutPinnedTabsView() subscribeToPinnedTabsViewModelOutputs() From 4914438d37af6e9d4a988b8190cb3ab884f0321a Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 19:47:29 -0300 Subject: [PATCH 17/27] Push JSON --- tab-bar-remote-message.json | 231 ++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 tab-bar-remote-message.json diff --git a/tab-bar-remote-message.json b/tab-bar-remote-message.json new file mode 100644 index 0000000000..4ab0822362 --- /dev/null +++ b/tab-bar-remote-message.json @@ -0,0 +1,231 @@ +{ + "version": 9, + "messages": [ + { + "id": "macos_permanent_survey_tab_bar", + "content": { + "messageType": "big_single_action", + "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": { + "type": "survey", + "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", + "additionalParameters": { + "queryParams": "delta;var;osv;ddgv" + } + } + }, + "matchingRules": [] + } + ], + "rules": [ + { + "id": 1, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproPurchasePlatform": { + "value": [ + "apple" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "expiring" + ] + }, + "pproDaysUntilExpiryOrRenewal": { + "max": 10 + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": true + } + } + }, + { + "id": 2, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproPurchasePlatform": { + "value": [ + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "expiring" + ] + }, + "pproDaysUntilExpiryOrRenewal": { + "max": 10 + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 3, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproDaysSinceSubscribed": { + "min": 14 + }, + "pproPurchasePlatform": { + "value": [ + "apple", + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": true + } + } + }, + { + "id": 4, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproDaysSinceSubscribed": { + "min": 14 + }, + "pproPurchasePlatform": { + "value": [ + "apple", + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 5, + "attributes": { + "interactedWithMessage": { + "value": [ + "macos_privacy_pro_exit_survey_1", + "macos_privacy_pro_sparkle_exit_survey_1", + "macos_privacy_pro_app_store_exit_survey_1" + ] + } + } + }, + { + "id": 6, + "attributes": { + "interactedWithMessage": { + "value": [ + "macos_privacy_pro_subscriber_survey_1" + ] + } + } + }, + { + "id": 7, + "attributes": { + "interactedWithDeprecatedMacRemoteMessage": { + "value": [ + "privacy_pro_exit_survey_1" + ] + } + } + }, + { + "id": 8, + "attributes": { + "interactedWithDeprecatedMacRemoteMessage": { + "value": [ + "privacy_pro_survey_1" + ] + } + } + }, + { + "id": 9, + "attributes": { + "appVersion": { + "min": "1.106.0" + }, + "daysSinceInstalled": { + "min": 14, + "max": 21 + }, + "locale": { + "value": [ + "en-US", + "en-CA", + "en-GB", + "en-AU" + ] + } + } + }, + { + "id": 10, + "attributes": { + "appVersion": { + "min": "1.99.0", + "max": "1.102.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 11, + "attributes": { + "appVersion": { + "min": "1.116.0.0", + "max": "1.116.0.999999" + }, + "locale": { + "value": [ + "en-US" + ] + }, + "pproSubscriber": { + "value": true + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + } + } + } + ] +} From f8252d1c97ee1633320ed7817811928a929f3378 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 17 Dec 2024 19:48:49 -0300 Subject: [PATCH 18/27] Add raw JSON for review build --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 265658a8a0..96b745dbbb 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,9 +47,9 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/4914438d37af6e9d4a988b8190cb3ab884f0321a/tab-bar-remote-message.json")! #else - URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/4914438d37af6e9d4a988b8190cb3ab884f0321a/tab-bar-remote-message.json")! #endif }() } From f3151bed2d064afb649a37365ccd9e7546c07065 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 18 Dec 2024 13:06:36 -0300 Subject: [PATCH 19/27] Address button feedback --- .../RemoteMessagingClient.swift | 2 +- .../TabBarRemoteMessageView.swift | 133 +++++------------- .../View/TabBarRemoteMessagePresenting.swift | 49 +++++++ 3 files changed, 87 insertions(+), 97 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 96b745dbbb..d54fbe8845 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,7 +47,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/4914438d37af6e9d4a988b8190cb3ab884f0321a/tab-bar-remote-message.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/4914438d37af6e9d4a988b8190cb3ab884f0321a/tab-bar-remote-message.json")! #endif diff --git a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift index 34d638a76b..76596d860f 100644 --- a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift @@ -19,9 +19,11 @@ import SwiftUI struct TabBarRemoteMessageView: View { - @State private var presentPopup: Bool = false + @State private var isHovered: Bool = false + @State private var isButtonHovered: Bool = false let model: TabBarRemoteMessage + let onClose: () -> Void let onTap: (URL) -> Void let onHover: () -> Void @@ -30,21 +32,40 @@ struct TabBarRemoteMessageView: View { var body: some View { HStack { - Button(model.buttonTitle) { - onTap(model.surveyURL) + Text(model.buttonTitle) + .font(.system(size: 13)) + .fixedSize(horizontal: true, vertical: false) + .foregroundColor(.white) + + Button(action: { onClose() }) { + Image(.close) + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + } + .frame(width: 16, height: 16) + .buttonStyle(PlainButtonStyle()) + .background(isButtonHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + .cornerRadius(2) + .onHover { hovering in + isButtonHovered = hovering + } + } + .padding(8) + .background(Color("PrimaryButtonRest")) + .frame(height: 24) + .cornerRadius(8) + .onAppear(perform: { onAppear() }) + .onHover { hovering in + isHovered = hovering + + if hovering { + onHover() + } else { + onHoverEnd() } - .buttonStyle(DefaultActionButtonStyle( - enabled: true, - onClose: { onClose() }, - onHoverStart: { - onHover() - }, - onHoverEnd: { - onHoverEnd() - }) - ) - .onAppear(perform: { onAppear() }) - .frame(width: 147) } } } @@ -77,86 +98,6 @@ struct TabBarRemoteMessagePopoverContent: View { .padding(.trailing, 12) .padding([.bottom, .top], 10) } - .frame(width: Constants.width, height: Constants.height) - } -} - -private struct DefaultActionButtonStyle: ButtonStyle { - - public let enabled: Bool - public let onClose: () -> Void - public let onHoverStart: () -> Void - public let onHoverEnd: () -> Void - - public init( - enabled: Bool, - onClose: @escaping () -> Void, - onHoverStart: @escaping () -> Void = {}, - onHoverEnd: @escaping () -> Void = {} - ) { - self.enabled = enabled - self.onClose = onClose - self.onHoverStart = onHoverStart - self.onHoverEnd = onHoverEnd - } - - public func makeBody(configuration: Self.Configuration) -> some View { - ButtonContent( - configuration: configuration, - enabled: enabled, - onClose: onClose, - onHoverStart: onHoverStart, - onHoverEnd: onHoverEnd - ) - } - - struct ButtonContent: View { - let configuration: Configuration - let enabled: Bool - let onClose: () -> Void - let onHoverStart: () -> Void - let onHoverEnd: () -> Void - - @State private var isHovered: Bool = false - - var body: some View { - let enabledBackgroundColor = configuration.isPressed - ? Color("PrimaryButtonPressed") - : (isHovered - ? Color("PrimaryButtonHover") - : Color("PrimaryButtonRest")) - - let disabledBackgroundColor = Color.gray.opacity(0.1) - let enabledLabelColor = configuration.isPressed ? Color.white.opacity(0.8) : Color.white - let disabledLabelColor = Color.primary.opacity(0.3) - - HStack(spacing: 5) { - configuration.label - .font(.system(size: 13)) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - - Button(action: { onClose() }) { - Image(.close) - } - .frame(width: 16, height: 16) - .buttonStyle(PlainButtonStyle()) - } - .frame(minWidth: 44) - .padding(.top, 2.5) - .padding(.bottom, 3) - .padding(.horizontal, 7.5) - .background(enabled ? enabledBackgroundColor : disabledBackgroundColor) - .foregroundColor(enabled ? enabledLabelColor : disabledLabelColor) - .cornerRadius(5) - .onHover { hovering in - isHovered = hovering - if hovering { - onHoverStart() - } else { - onHoverEnd() - } - } - } + .frame(minWidth: Constants.width, minHeight: Constants.height) } } diff --git a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift index 294bf6f4b9..8ced06d7ce 100644 --- a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift +++ b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift @@ -19,6 +19,18 @@ 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 } @@ -30,6 +42,11 @@ protocol TabBarRemoteMessagePresenting: AnyObject { 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 @@ -45,6 +62,13 @@ extension TabBarRemoteMessagePresenting { }) } + /// 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, @@ -83,6 +107,12 @@ extension TabBarRemoteMessagePresenting { ]) } + /// 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) { _ in @@ -90,11 +120,20 @@ extension TabBarRemoteMessagePresenting { } } + /// 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 @@ -110,6 +149,12 @@ extension TabBarRemoteMessagePresenting { } } + /// 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 } @@ -123,6 +168,10 @@ extension TabBarRemoteMessagePresenting { 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 } From cde71c39ee4620731ffeca39f403185bc6e4163a Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 19 Dec 2024 10:51:26 -0300 Subject: [PATCH 20/27] Make popover resize automatically --- .../TabBarRemoteMessageView.swift | 35 +++++++++---------- .../View/TabBarRemoteMessagePresenting.swift | 7 ++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift index 76596d860f..4d9ed3e5fd 100644 --- a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift @@ -19,8 +19,8 @@ import SwiftUI struct TabBarRemoteMessageView: View { - @State private var isHovered: Bool = false - @State private var isButtonHovered: Bool = false + @State private var wasViewHovered: Bool = false + @State private var wasCloseButtonHovered: Bool = false let model: TabBarRemoteMessage @@ -45,21 +45,23 @@ struct TabBarRemoteMessageView: View { } .frame(width: 16, height: 16) .buttonStyle(PlainButtonStyle()) - .background(isButtonHovered + .background(wasCloseButtonHovered && !wasViewHovered ? Color("PrimaryButtonHover") : Color("PrimaryButtonRest")) .cornerRadius(2) .onHover { hovering in - isButtonHovered = hovering + wasCloseButtonHovered = hovering } } .padding(8) - .background(Color("PrimaryButtonRest")) + .background(wasViewHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) .frame(height: 24) .cornerRadius(8) .onAppear(perform: { onAppear() }) .onHover { hovering in - isHovered = hovering + wasViewHovered = hovering if hovering { onHover() @@ -71,33 +73,28 @@ struct TabBarRemoteMessageView: View { } struct TabBarRemoteMessagePopoverContent: View { - enum Constants { - static let height: CGFloat = 92 - static let width: CGFloat = 360 - } - let model: TabBarRemoteMessage var body: some View { - HStack(alignment: .center, spacing: 0) { + HStack(alignment: .center, spacing: 12) { Image(.daxResponse) .resizable() .scaledToFit() .frame(width: 72, height: 72) - .padding(.leading, 8) - .padding(.trailing, 16) - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { Text(model.popupTitle) .font(.system(size: 13, weight: .bold)) - .padding(.bottom, 8) + .padding(.top, 9) Text(model.popupSubtitle) .font(.system(size: 13, weight: .medium)) + .padding(.bottom, 9) } - .padding(.trailing, 12) - .padding([.bottom, .top], 10) + .frame(width: 236) } - .frame(minWidth: Constants.width, minHeight: Constants.height) + .padding([.top, .bottom], 10) + .padding(.leading, 12) + .padding(.trailing, 24) } } diff --git a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift index 8ced06d7ce..23c417bcc0 100644 --- a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift +++ b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift @@ -158,13 +158,12 @@ extension TabBarRemoteMessagePresenting { 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 = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, - height: TabBarRemoteMessagePopoverContent.Constants.height) - + popover.contentSize = NSHostingView(rootView: contentView).fittingSize let controller = NSViewController() - controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + controller.view = NSHostingView(rootView: contentView) popover.contentViewController = controller } From d68d8a468abaa7932ae784e935e32c4dc065e605 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 19 Dec 2024 11:29:20 -0300 Subject: [PATCH 21/27] Implement new button designs --- .../TabBarRemoteMessageView.swift | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift index 4d9ed3e5fd..5332b13456 100644 --- a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift @@ -31,44 +31,55 @@ struct TabBarRemoteMessageView: View { let onAppear: () -> Void var body: some View { - HStack { - Text(model.buttonTitle) - .font(.system(size: 13)) - .fixedSize(horizontal: true, vertical: false) - .foregroundColor(.white) + 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, 4) + .cornerRadius(8) + .background(wasViewHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + .onTapGesture { onTap(model.surveyURL) } + .onHover { hovering in + wasViewHovered = hovering + + if hovering { + onHover() + } else { + onHoverEnd() + } + } - Button(action: { onClose() }) { + HStack { Image(.close) .resizable() .scaledToFit() .frame(width: 16, height: 16) } - .frame(width: 16, height: 16) - .buttonStyle(PlainButtonStyle()) - .background(wasCloseButtonHovered && !wasViewHovered + .padding([.top, .bottom, .trailing], 8) + .background(wasCloseButtonHovered ? Color("PrimaryButtonHover") : Color("PrimaryButtonRest")) - .cornerRadius(2) + .cornerRadius(8) + .onTapGesture { + onClose() + } .onHover { hovering in wasCloseButtonHovered = hovering } + .frame(maxWidth: .infinity) } - .padding(8) - .background(wasViewHovered + .background(wasCloseButtonHovered || wasViewHovered ? Color("PrimaryButtonHover") : Color("PrimaryButtonRest")) .frame(height: 24) .cornerRadius(8) .onAppear(perform: { onAppear() }) - .onHover { hovering in - wasViewHovered = hovering - - if hovering { - onHover() - } else { - onHoverEnd() - } - } } } From 8b910130484b8ae3db1ede059bd4e756447a5788 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 19 Dec 2024 12:19:19 -0300 Subject: [PATCH 22/27] Use weak self on closures --- .../View/TabBarRemoteMessagePresenting.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift index 23c417bcc0..629341b1b1 100644 --- a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift +++ b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresenting.swift @@ -72,24 +72,31 @@ extension TabBarRemoteMessagePresenting { private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { let feedbackButtonView = TabBarRemoteMessageView( model: tabBarRemotMessage, - onClose: { + onClose: { [weak self] in + guard let self = self else { return } + self.tabBarRemoteMessageViewModel.onMessageDismissed() self.removeFeedbackButton() }, - onTap: { surveyURL in + 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: { + onHover: { [weak self] in + guard let self = self else { return } self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) }, - onHoverEnd: { + onHoverEnd: { [weak self] in + guard let self = self else { return } self.dismissTabBarRemoteMessagePopover() }, - onAppear: { + onAppear: { [weak self] in + guard let self = self else { return } self.tabBarRemoteMessageViewModel.markTabBarRemoteMessageAsShown() } ) @@ -115,7 +122,8 @@ extension TabBarRemoteMessagePresenting { /// - Parameter message: The remote message associated with the popover private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in + guard let self = self else { return } self.showTabBarRemotePopup(message) } } From d56ee4dea2874b695a1c69d6e3aee1538e2f7cd8 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 19 Dec 2024 14:02:16 -0300 Subject: [PATCH 23/27] Create two publishers: one for NTP and another for tab bar --- DuckDuckGo/HomePage/View/HomePageView.swift | 2 +- .../ActiveRemoteMessageModel+NewTabPage.swift | 3 +- .../ActiveRemoteMessageModel.swift | 19 ++++++++- .../TabBarActiveRemoteMessage.swift | 2 +- .../TabBarRemoteMessageViewModel.swift | 2 +- ...wTabPageActiveRemoteMessageProviding.swift | 2 +- .../NewTabPage/RMF/NewTabPageRMFClient.swift | 12 +++--- ...ewTabPageActiveRemoteMessageProvider.swift | 4 +- .../NewTabPageRMFClientTests.swift | 28 ++++++------- .../ActiveRemoteMessageModelTests.swift | 41 +++++++++++++++++-- 10 files changed, 83 insertions(+), 32 deletions(-) diff --git a/DuckDuckGo/HomePage/View/HomePageView.swift b/DuckDuckGo/HomePage/View/HomePageView.swift index 8fe1f5ff25..0adbcc6302 100644 --- a/DuckDuckGo/HomePage/View/HomePageView.swift +++ b/DuckDuckGo/HomePage/View/HomePageView.swift @@ -183,7 +183,7 @@ extension HomePage.Views { @ViewBuilder func remoteMessage() -> some View { - if let remoteMessage = activeRemoteMessageModel.remoteMessage, + if let remoteMessage = activeRemoteMessageModel.newTabPageRemoteMessage, !remoteMessage.isForTabBar, let modelType = remoteMessage.content, modelType.isSupported { diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift index 42e621054a..24475d092a 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -22,9 +22,8 @@ import RemoteMessaging extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { var remoteMessagePublisher: AnyPublisher { - $remoteMessage + $newTabPageRemoteMessage .dropFirst() - .filter { $0?.isForTabBar == false } .eraseToAnyPublisher() } diff --git a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift index bf933c1774..fcd93c7c7a 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 } diff --git a/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift b/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift index 9ee658c0d0..10c4ffa877 100644 --- a/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift +++ b/DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift @@ -31,7 +31,7 @@ final class TabBarActiveRemoteMessage: TabBarRemoteMessageProviding { private let activeRemoteMessageModel: ActiveRemoteMessageModel var remoteMessagePublisher: AnyPublisher { - activeRemoteMessageModel.$remoteMessage.eraseToAnyPublisher() + activeRemoteMessageModel.$tabBarRemoteMessage.eraseToAnyPublisher() } init(activeRemoteMessageModel: ActiveRemoteMessageModel) { diff --git a/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift index 0a8f3c3eff..4d2754a7ed 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabBarRemoteMessageViewModel.swift @@ -62,7 +62,7 @@ private extension RemoteMessageModel { var shouldShowTabBarRemoteMessage: Bool { guard let modelType = content else { return false } - return modelType.isSupported && isForTabBar + return modelType.isSupported } func mapToTabBarRemoteMessage() -> TabBarRemoteMessage? { diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift index 8035ae7d52..32a0094a2b 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift @@ -20,7 +20,7 @@ import Combine import RemoteMessaging public protocol NewTabPageActiveRemoteMessageProviding { - var remoteMessage: RemoteMessageModel? { get set } + var newTabPageRemoteMessage: RemoteMessageModel? { get set } var remoteMessagePublisher: 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..37b601433e 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift @@ -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..16068dc4c3 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() + $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) + } } From 7d54df276ddc59e497a17c4d9dbd2e624cbcf3ca Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 19 Dec 2024 15:43:35 -0300 Subject: [PATCH 24/27] Address minor design feedback around margins --- .../RemoteMessaging/TabBarRemoteMessageView.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift index 5332b13456..b08d56b530 100644 --- a/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift @@ -39,7 +39,7 @@ struct TabBarRemoteMessageView: View { .foregroundColor(.white) } .padding([.leading, .top, .bottom], 8) - .padding(.trailing, 4) + .padding(.trailing, 6) .cornerRadius(8) .background(wasViewHovered ? Color("PrimaryButtonHover") @@ -55,13 +55,20 @@ struct TabBarRemoteMessageView: View { } } + 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, .trailing], 8) + .padding([.top, .bottom]) + .padding([.leading, .trailing], 4) .background(wasCloseButtonHovered ? Color("PrimaryButtonHover") : Color("PrimaryButtonRest")) @@ -102,8 +109,8 @@ struct TabBarRemoteMessagePopoverContent: View { .font(.system(size: 13, weight: .medium)) .padding(.bottom, 9) } - .frame(width: 236) } + .frame(width: 360) .padding([.top, .bottom], 10) .padding(.leading, 12) .padding(.trailing, 24) From ef287f4aedfcfb13955fec2a1dd0cf198e94d92d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 2 Jan 2025 11:46:12 +0100 Subject: [PATCH 25/27] Rename remoteMessagePublisher to newTabPageRemoteMessagePublisher in NewTabPageActiveRemoteMessageProviding --- DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift | 2 +- .../NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift | 2 +- .../NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift | 2 +- .../Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift index 24475d092a..1fd02423dd 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -21,7 +21,7 @@ import NewTabPage import RemoteMessaging extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { - var remoteMessagePublisher: AnyPublisher { + var newTabPageRemoteMessagePublisher: AnyPublisher { $newTabPageRemoteMessage .dropFirst() .eraseToAnyPublisher() diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift index 32a0094a2b..9fa892478a 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift @@ -21,7 +21,7 @@ import RemoteMessaging public protocol NewTabPageActiveRemoteMessageProviding { var newTabPageRemoteMessage: RemoteMessageModel? { get set } - var remoteMessagePublisher: AnyPublisher { get } + 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 37b601433e..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) } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift index 16068dc4c3..165f036f1c 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift @@ -24,7 +24,7 @@ import NewTabPage final class CapturingNewTabPageActiveRemoteMessageProvider: NewTabPageActiveRemoteMessageProviding { @Published var newTabPageRemoteMessage: RemoteMessageModel? - var remoteMessagePublisher: AnyPublisher { + var newTabPageRemoteMessagePublisher: AnyPublisher { $newTabPageRemoteMessage.dropFirst().eraseToAnyPublisher() } From 5a64cc3aef8284aa0f4f16c40ef159abaf9e6854 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 3 Jan 2025 12:29:31 -0300 Subject: [PATCH 26/27] Revert remote JSON testing files --- .../RemoteMessagingClient.swift | 4 +- tab-bar-remote-message.json | 231 ------------------ 2 files changed, 2 insertions(+), 233 deletions(-) delete mode 100644 tab-bar-remote-message.json diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index d54fbe8845..3289912570 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,9 +47,9 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! #else - URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/4914438d37af6e9d4a988b8190cb3ab884f0321a/tab-bar-remote-message.json")! + URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! #endif }() } diff --git a/tab-bar-remote-message.json b/tab-bar-remote-message.json deleted file mode 100644 index 4ab0822362..0000000000 --- a/tab-bar-remote-message.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "version": 9, - "messages": [ - { - "id": "macos_permanent_survey_tab_bar", - "content": { - "messageType": "big_single_action", - "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": { - "type": "survey", - "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", - "additionalParameters": { - "queryParams": "delta;var;osv;ddgv" - } - } - }, - "matchingRules": [] - } - ], - "rules": [ - { - "id": 1, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproPurchasePlatform": { - "value": [ - "apple" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "expiring" - ] - }, - "pproDaysUntilExpiryOrRenewal": { - "max": 10 - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": true - } - } - }, - { - "id": 2, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproPurchasePlatform": { - "value": [ - "stripe" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "expiring" - ] - }, - "pproDaysUntilExpiryOrRenewal": { - "max": 10 - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": false - } - } - }, - { - "id": 3, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproDaysSinceSubscribed": { - "min": 14 - }, - "pproPurchasePlatform": { - "value": [ - "apple", - "stripe" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "active" - ] - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": true - } - } - }, - { - "id": 4, - "attributes": { - "pproSubscriber": { - "value": true - }, - "pproDaysSinceSubscribed": { - "min": 14 - }, - "pproPurchasePlatform": { - "value": [ - "apple", - "stripe" - ] - }, - "pproSubscriptionStatus": { - "value": [ - "active" - ] - }, - "appVersion": { - "min": "1.101.0" - }, - "installedMacAppStore": { - "value": false - } - } - }, - { - "id": 5, - "attributes": { - "interactedWithMessage": { - "value": [ - "macos_privacy_pro_exit_survey_1", - "macos_privacy_pro_sparkle_exit_survey_1", - "macos_privacy_pro_app_store_exit_survey_1" - ] - } - } - }, - { - "id": 6, - "attributes": { - "interactedWithMessage": { - "value": [ - "macos_privacy_pro_subscriber_survey_1" - ] - } - } - }, - { - "id": 7, - "attributes": { - "interactedWithDeprecatedMacRemoteMessage": { - "value": [ - "privacy_pro_exit_survey_1" - ] - } - } - }, - { - "id": 8, - "attributes": { - "interactedWithDeprecatedMacRemoteMessage": { - "value": [ - "privacy_pro_survey_1" - ] - } - } - }, - { - "id": 9, - "attributes": { - "appVersion": { - "min": "1.106.0" - }, - "daysSinceInstalled": { - "min": 14, - "max": 21 - }, - "locale": { - "value": [ - "en-US", - "en-CA", - "en-GB", - "en-AU" - ] - } - } - }, - { - "id": 10, - "attributes": { - "appVersion": { - "min": "1.99.0", - "max": "1.102.0" - }, - "installedMacAppStore": { - "value": false - } - } - }, - { - "id": 11, - "attributes": { - "appVersion": { - "min": "1.116.0.0", - "max": "1.116.0.999999" - }, - "locale": { - "value": [ - "en-US" - ] - }, - "pproSubscriber": { - "value": true - }, - "pproSubscriptionStatus": { - "value": [ - "active" - ] - } - } - } - ] -} From e41a3f6bb78b7a5454e31ef89be485e08d53fdba Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 3 Jan 2025 12:33:18 -0300 Subject: [PATCH 27/27] Remove usage of isForTabBar outside model --- DuckDuckGo/HomePage/View/HomePageView.swift | 1 - DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/DuckDuckGo/HomePage/View/HomePageView.swift b/DuckDuckGo/HomePage/View/HomePageView.swift index 0adbcc6302..f6ddd2f35f 100644 --- a/DuckDuckGo/HomePage/View/HomePageView.swift +++ b/DuckDuckGo/HomePage/View/HomePageView.swift @@ -184,7 +184,6 @@ extension HomePage.Views { @ViewBuilder func remoteMessage() -> some View { if let remoteMessage = activeRemoteMessageModel.newTabPageRemoteMessage, - !remoteMessage.isForTabBar, let modelType = remoteMessage.content, modelType.isSupported { ZStack { diff --git a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift index fcd93c7c7a..b49a6c19c8 100644 --- a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift +++ b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift @@ -203,7 +203,7 @@ extension RemoteMessageModelType { } } -extension RemoteMessageModel { +private extension RemoteMessageModel { var isForTabBar: Bool { return id == TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId