diff --git a/Storage/Storage/Model/FeedbackType.swift b/Storage/Storage/Model/FeedbackType.swift index fa71f7fe53e..5e834292835 100644 --- a/Storage/Storage/Model/FeedbackType.swift +++ b/Storage/Storage/Model/FeedbackType.swift @@ -16,4 +16,8 @@ public enum FeedbackType: String, Codable { /// Identifier for the orders creation feedback survey /// case ordersCreation + + /// Identifier for the In-Person Payments feedback survey + /// + case IPP } diff --git a/WooCommerce/Classes/System/WooConstants.swift b/WooCommerce/Classes/System/WooConstants.swift index f1f3986e7bd..219204addaf 100644 --- a/WooCommerce/Classes/System/WooConstants.swift +++ b/WooCommerce/Classes/System/WooConstants.swift @@ -155,8 +155,10 @@ extension WooConstants { /// #if DEBUG case inAppFeedback = "https://automattic.survey.fm/woo-app-general-feedback-test-survey" + case IPPFeedback = "https://automattic.survey.fm/woo-app-ipp-in-app-feedback-testing" #else case inAppFeedback = "https://automattic.survey.fm/woo-app-general-feedback-user-survey" + case IPPFeedback = "https://automattic.survey.fm/woo-app-ipp-in-app-feedback-testing" #endif /// URL for the products feedback survey diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift index 59740f96c0f..f069f563f29 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift @@ -254,6 +254,8 @@ private extension OrderListViewController { self.setErrorTopBanner() case .orderCreation: self.setOrderCreationTopBanner() + case .IPPFeedback: + self.setIPPFeedbackTopBanner() } } .store(in: &cancellables) @@ -783,6 +785,37 @@ private extension OrderListViewController { }) showTopBannerView() } + + /// Sets the `topBannerView` property to an IPP feedback banner. + /// + func setIPPFeedbackTopBanner() { + topBannerView = createIPPFeedbackTopBanner() + showTopBannerView() + } + + private func createIPPFeedbackTopBanner() -> TopBannerView { + let shareIPPFeedbackAction = TopBannerViewModel.ActionButton(title: Localization.shareFeedbackButton, action: { _ in + self.displayIPPFeedbackBannerSurvey() + }) + + let viewModel = TopBannerViewModel( + title: Localization.feedbackBannerTitle, + infoText: Localization.feedbackBannerContent, + icon: UIImage.gridicon(.comment), + isExpanded: true, + topButton: .dismiss(handler: { }), + actionButtons: [shareIPPFeedbackAction] + ) + let topBannerView = TopBannerView(viewModel: viewModel) + topBannerView.translatesAutoresizingMaskIntoConstraints = false + return topBannerView + } + + private func displayIPPFeedbackBannerSurvey() { + // TODO: Survey will change based on conditions + let surveyNavigation = SurveyCoordinatingController(survey: .IPPFeedback) + self.present(surveyNavigation, animated: true, completion: nil) + } } // MARK: - Constants @@ -801,6 +834,18 @@ private extension OrderListViewController { static let markCompleted = NSLocalizedString("Mark Completed", comment: "Title for the swipe order action to mark it as completed") + static let feedbackBannerTitle = NSLocalizedString("Let us know what you think", + comment: "Title of the In-Person Payments feedback banner in the Orders tab" + ) + + static let feedbackBannerContent = NSLocalizedString("Rate your In-Person Payment experience.", + comment: "Content of the In-Person Payments feedback banner in the Orders tab" + ) + + static let shareFeedbackButton = NSLocalizedString("Share feedback", + comment: "Title of the feedback action button on the In-Person Payments feedback banner" + ) + static func markCompletedNoticeTitle(orderID: Int64) -> String { let format = NSLocalizedString( "Order #%1$d marked as completed", diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index f53dd24cdb1..79b9e7922ba 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -131,6 +131,11 @@ final class OrderListViewModel { /// @Published var hideOrdersBanners: Bool = true + /// If true, no IPP feedback banner will be shown as the user has told us that they are not interested in this information. + /// It is persisted through app sessions. + /// + @Published var hideIPPFeedbackBanner: Bool = true + init(siteID: Int64, stores: StoresManager = ServiceLocator.stores, storageManager: StorageManagerType = ServiceLocator.storageManager, @@ -143,6 +148,10 @@ final class OrderListViewModel { self.pushNotificationsManager = pushNotificationsManager self.notificationCenter = notificationCenter self.filters = filters + + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) && !hideIPPFeedbackBanner { + topBanner = .IPPFeedback + } } deinit { @@ -165,10 +174,12 @@ final class OrderListViewModel { observeForegroundRemoteNotifications() bindTopBannerState() - loadOrdersBannerVisibility() if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) { - fetchIPPTransactions() + syncIPPBannerVisibility() + loadOrdersBannerVisibility() + } else { + loadOrdersBannerVisibility() } } @@ -208,6 +219,30 @@ final class OrderListViewModel { stores.dispatch(action) } + // This is a temporary method in order to update the IPP feedback status to `.pending`, and + // then load feedback visibility. We need to reset the banner status on UserDefaults for + // the banner to appear again for testing purposes. + private func syncIPPBannerVisibility() { + let action = AppSettingsAction.updateFeedbackStatus(type: .IPP, status: .pending) { _ in + self.loadIPPFeedbackBannerVisibility() + self.fetchIPPTransactions() + } + stores.dispatch(action) + } + + private func loadIPPFeedbackBannerVisibility() { + let action = AppSettingsAction.loadFeedbackVisibility(type: .IPP) { [weak self] result in + switch result { + case .success(let visible): + self?.hideIPPFeedbackBanner = !visible + case .failure(let error): + self?.hideIPPFeedbackBanner = true + ServiceLocator.crashLogging.logError(error) + } + } + self.stores.dispatch(action) + } + @objc private func handleAppDeactivation() { isAppActive = false } @@ -365,18 +400,18 @@ extension OrderListViewModel { private func bindTopBannerState() { let errorState = $hasErrorLoadingData.removeDuplicates() - Publishers.CombineLatest(errorState, $hideOrdersBanners) - .map { hasError, hasDismissedOrdersBanners -> TopBanner in + Publishers.CombineLatest3(errorState, $hideIPPFeedbackBanner, $hideOrdersBanners) + .map { hasError, hasDismissedIPPFeedbackBanner, hasDismissedOrdersBanners -> TopBanner in - if hasError { + guard !hasError else { return .error } - if hasDismissedOrdersBanners { - return .none + guard hasDismissedIPPFeedbackBanner else { + return .IPPFeedback } - return .orderCreation + return hasDismissedOrdersBanners ? .none : .orderCreation } .assign(to: &$topBanner) } @@ -419,6 +454,7 @@ extension OrderListViewModel { enum TopBanner { case error case orderCreation + case IPPFeedback case none } } diff --git a/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift b/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift index 96fac6c4f03..767159510ac 100644 --- a/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift @@ -68,6 +68,7 @@ extension SurveyViewController { case addOnsI1 case orderCreation case couponManagement + case IPPFeedback fileprivate var url: URL { switch self { @@ -102,6 +103,11 @@ extension SurveyViewController { .asURL() .tagPlatform("ios") .tagAppVersion(Bundle.main.bundleVersion()) + case .IPPFeedback: + return WooConstants.URLs.IPPFeedback + .asURL() + .tagPlatform("ios") + .tagAppVersion(Bundle.main.bundleVersion()) } } @@ -109,7 +115,7 @@ extension SurveyViewController { switch self { case .inAppFeedback: return Localization.title - case .productsFeedback, .shippingLabelsRelease3Feedback, .addOnsI1, .orderCreation, .couponManagement: + case .productsFeedback, .shippingLabelsRelease3Feedback, .addOnsI1, .orderCreation, .couponManagement, .IPPFeedback: return Localization.giveFeedback } } @@ -117,7 +123,7 @@ extension SurveyViewController { /// The corresponding `FeedbackContext` for event tracking purposes. var feedbackContextForEvents: WooAnalyticsEvent.FeedbackContext { switch self { - case .inAppFeedback: + case .inAppFeedback, .IPPFeedback: return .general case .productsFeedback: return .productsGeneral diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/OrderListViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/OrderListViewModelTests.swift index 4ec3b11e5ac..c0f939aca52 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/OrderListViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/OrderListViewModelTests.swift @@ -239,7 +239,7 @@ final class OrderListViewModelTests: XCTestCase { XCTAssertFalse(resynchronizeRequested) } - func test_when_having_no_error__and_orders_banner_should_not_be_shown_shows_nothing() { + func test_when_having_no_error_and_orders_banner_should_not_be_shown_shows_nothing() { // Given let viewModel = OrderListViewModel(siteID: siteID, stores: stores, filters: nil) stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in @@ -253,6 +253,7 @@ final class OrderListViewModelTests: XCTestCase { // When viewModel.activate() + viewModel.hideIPPFeedbackBanner = true // Then waitUntil { @@ -260,6 +261,28 @@ final class OrderListViewModelTests: XCTestCase { } } + func test_when_having_no_error_and_IPP_banner_should_be_shown_shows_IPP_banner() { + // Given + let viewModel = OrderListViewModel(siteID: siteID, stores: stores, filters: nil) + stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in + switch action { + case let .loadFeedbackVisibility(.IPP, onCompletion): + onCompletion(.success(true)) + default: + break + } + } + + // When + viewModel.activate() + viewModel.hideIPPFeedbackBanner = false + + // Then + waitUntil { + viewModel.topBanner == .IPPFeedback + } + } + func test_when_having_no_error_and_orders_banner_should_be_shown_shows_orders_banner() { // Given let viewModel = OrderListViewModel(siteID: siteID, stores: stores, filters: nil) @@ -274,6 +297,7 @@ final class OrderListViewModelTests: XCTestCase { // When viewModel.activate() + viewModel.hideIPPFeedbackBanner = true // Then waitUntil { @@ -281,6 +305,36 @@ final class OrderListViewModelTests: XCTestCase { } } + func test_when_having_no_error_and_orders_banner_or_IPP_banner_should_be_shown_shows_correct_banner() { + // Given + let isIPPFeatureFlagEnabled = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) + let viewModel = OrderListViewModel(siteID: siteID, stores: stores, filters: nil) + + stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in + switch action { + case let .loadFeedbackVisibility(.ordersCreation, onCompletion): + onCompletion(.success(true)) + default: + break + } + } + + // When + viewModel.activate() + + // Then + if isIPPFeatureFlagEnabled { + viewModel.hideIPPFeedbackBanner = false + waitUntil { + viewModel.topBanner == .IPPFeedback + } + } else { + waitUntil { + viewModel.topBanner == .orderCreation + } + } + } + func test_when_having_no_error_and_orders_banner_visibility_loading_fails_shows_nothing() { // Given let viewModel = OrderListViewModel(siteID: siteID, stores: stores, filters: nil) @@ -317,12 +371,13 @@ final class OrderListViewModelTests: XCTestCase { } } - func test_dismissing_orders_banners_does_not_show_banners() { + func test_dismissing_banners_does_not_show_banners() { // Given let viewModel = OrderListViewModel(siteID: siteID, stores: stores, filters: nil) // When viewModel.activate() + viewModel.hideIPPFeedbackBanner = true viewModel.hideOrdersBanners = true // Then diff --git a/Yosemite/Yosemite/Stores/AppSettings/InAppFeedbackCardVisibilityUseCase.swift b/Yosemite/Yosemite/Stores/AppSettings/InAppFeedbackCardVisibilityUseCase.swift index da44491564d..adfed30e640 100644 --- a/Yosemite/Yosemite/Stores/AppSettings/InAppFeedbackCardVisibilityUseCase.swift +++ b/Yosemite/Yosemite/Stores/AppSettings/InAppFeedbackCardVisibilityUseCase.swift @@ -37,7 +37,7 @@ struct InAppFeedbackCardVisibilityUseCase { switch feedbackType { case .general: return try shouldGeneralFeedbackBeVisible(currentDate: currentDate) - case .shippingLabelsRelease3, .couponManagement, .ordersCreation: + case .shippingLabelsRelease3, .couponManagement, .ordersCreation, .IPP: return settings.feedbackStatus(of: feedbackType) == .pending } }