diff --git a/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift b/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift index f2430cbd4f7..b52cc58281a 100644 --- a/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift +++ b/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift @@ -5,6 +5,7 @@ public enum FeatureAnnouncementCampaign: String, Codable, Equatable { case linkedProductsPromo = "linked_products_promo" case paymentsInMenuTabBarButton = "payments_menu_tabbar_button" case paymentsInHubMenuButton = "payments_hub_menu_button" + case productsOnboarding = "products_onboarding_first_product" /// Added for use in `test_setFeatureAnnouncementDismissed_with_another_campaign_previously_dismissed_keeps_values_for_both` /// This can be removed when we have a second campaign, which can be used in the above test instead. diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/FeatureAnnouncementCardViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/FeatureAnnouncementCardViewModel.swift index b7f420fc546..181a3e58a80 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/FeatureAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/FeatureAnnouncementCardViewModel.swift @@ -17,7 +17,6 @@ protocol AnnouncementCardViewModelProtocol { func onAppear() func ctaTapped() - var showDismissButton: Bool { get } var showDismissConfirmation: Bool { get } var dismissAlertTitle: String { get } var dismissAlertMessage: String { get } @@ -46,8 +45,6 @@ class FeatureAnnouncementCardViewModel: AnnouncementCardViewModelProtocol { config.image } - var showDismissButton: Bool = true - var showDismissConfirmation: Bool { config.showDismissConfirmation } diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift index e1c1b3d487e..0762d60894e 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift @@ -61,9 +61,7 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode let image: UIImage = .paymentsFeatureBannerImage - var showDismissButton: Bool = true - - let showDismissConfirmation: Bool = false + var showDismissConfirmation: Bool = false let dismissAlertTitle: String = "" diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift index 9169bbbad7d..b8f68ae1bdb 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift @@ -1,8 +1,9 @@ import Foundation import UIKit +import enum Yosemite.AppSettingsAction struct ProductsOnboardingAnnouncementCardViewModel: AnnouncementCardViewModelProtocol { - var showDividers: Bool = false + var showDividers: Bool = true var badgeType: BadgeView.BadgeType = .tip @@ -27,12 +28,15 @@ struct ProductsOnboardingAnnouncementCardViewModel: AnnouncementCardViewModelPro onCTATapped?() } - // MARK: Dismiss button (disabled) - - var showDismissButton: Bool = false + // MARK: Dismiss button + /// Ensures the banner isn't shown again after the user manually dismisses it. + /// func dontShowAgainTapped() { - // No-op + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .productsOnboarding, + remindLater: false, + onCompletion: nil) + ServiceLocator.stores.dispatch(action) } // MARK: Dismiss confirmation alert (disabled) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift index 63fe02ea426..6654c95092e 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift @@ -124,18 +124,14 @@ final class DashboardViewModel { if isEligible { ServiceLocator.analytics.track(event: .ProductsOnboarding.storeIsEligible()) - if ABTest.productsOnboardingBanner.variation == .treatment(nil) { - let viewModel = ProductsOnboardingAnnouncementCardViewModel(onCTATapped: { [weak self] in - self?.announcementViewModel = nil // Dismiss announcement - MainTabBarController.presentAddProductFlow() - }) - self?.announcementViewModel = viewModel - // For now, products onboarding takes precedence over Just In Time Messages, - // so we can stop if there is an onboarding announcement to display. - // This should be revisited when either onboarding or JITMs are expanded. See: - // pe5pgL-11B-p2 - return - } + self?.setProductsOnboardingBannerIfNeeded() + } + + // For now, products onboarding takes precedence over Just In Time Messages, + // so we can stop if there is an onboarding announcement to display. + // This should be revisited when either onboarding or JITMs are expanded. See: pe5pgL-11B-p2 + if self?.announcementViewModel is ProductsOnboardingAnnouncementCardViewModel { + return } onCompletion() case .failure(let error): @@ -146,6 +142,27 @@ final class DashboardViewModel { stores.dispatch(action) } + /// Sets the view model for the products onboarding banner if the user hasn't dismissed it before, + /// and if the user is part of the treatment group for the products onboarding A/B test. + /// + private func setProductsOnboardingBannerIfNeeded() { + guard ABTest.productsOnboardingBanner.variation == .treatment(nil) else { + return + } + + let getVisibility = AppSettingsAction.getFeatureAnnouncementVisibility(campaign: .productsOnboarding) { [weak self] result in + guard let self else { return } + if case let .success(isVisible) = result, isVisible { + let viewModel = ProductsOnboardingAnnouncementCardViewModel(onCTATapped: { [weak self] in + self?.announcementViewModel = nil // Dismiss announcement + MainTabBarController.presentAddProductFlow() + }) + self.announcementViewModel = viewModel + } + } + stores.dispatch(getVisibility) + } + /// Checks for Just In Time Messages and prepares the announcement if needed. /// private func syncJustInTimeMessages(for siteID: Int64) { diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift index 452b32b0a1f..e1f6de74d85 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift @@ -30,7 +30,7 @@ struct FeatureAnnouncementCardView: View { BadgeView(type: viewModel.badgeType) .padding(.leading, Layout.padding) Spacer() - if viewModel.showDismissButton, let dismiss = dismiss { + if let dismiss = dismiss { Button(action: { if viewModel.showDismissConfirmation { showingDismissActionSheet = true diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift index f554eb715a2..29302c669a1 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift @@ -2,6 +2,7 @@ import XCTest import enum Networking.DotcomError import enum Yosemite.StatsActionV4 import enum Yosemite.ProductAction +import enum Yosemite.AppSettingsAction import enum Yosemite.JustInTimeMessageAction import struct Yosemite.JustInTimeMessage @testable import WooCommerce @@ -99,6 +100,14 @@ final class DashboardViewModelTests: XCTestCase { XCTFail("Received unsupported action: \(action)") } } + stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in + switch action { + case let .getFeatureAnnouncementVisibility(_, completion): + completion(.success(true)) + default: + XCTFail("Received unsupported action: \(action)") + } + } stores.whenReceivingAction(ofType: JustInTimeMessageAction.self) { action in switch action { case let .loadMessage(_, _, _, completion): @@ -116,6 +125,34 @@ final class DashboardViewModelTests: XCTestCase { XCTAssertEqual(viewModel.announcementViewModel?.image, .emptyProductsImage) } + func test_onboarding_announcement_not_displayed_when_previously_dismissed() { + // Given + MockABTesting.setVariation(.treatment(nil), for: .productsOnboardingBanner) + stores.whenReceivingAction(ofType: ProductAction.self) { action in + switch action { + case let .checkProductsOnboardingEligibility(_, completion): + completion(.success(true)) + default: + XCTFail("Received unsupported action: \(action)") + } + } + stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in + switch action { + case let .getFeatureAnnouncementVisibility(_, completion): + completion(.success(false)) + default: + XCTFail("Received unsupported action: \(action)") + } + } + let viewModel = DashboardViewModel(stores: stores) + + // When + viewModel.syncAnnouncements(for: sampleSiteID) + + // Then + XCTAssertNil(viewModel.announcementViewModel) + } + func test_view_model_syncs_just_in_time_messages_when_ineligible_for_products_onboarding() { // Given let message = Yosemite.JustInTimeMessage.fake().copy(title: "JITM Message")