diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index adad4f99bea..8be9e5053ca 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -45,6 +45,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return true case .systemStatusReportInSupportRequest: return true + case .IPPInAppFeedbackBanner: + return buildConfig == .localDeveloper || buildConfig == .alpha case .performanceMonitoring, .performanceMonitoringCoreData, .performanceMonitoringFileIO, diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 32a7b4eb254..1bb1df2b2b3 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -99,6 +99,10 @@ public enum FeatureFlag: Int { /// case systemStatusReportInSupportRequest + /// IPP in-app feedback banner + /// + case IPPInAppFeedbackBanner + // MARK: - Performance Monitoring // // These flags are not transient. That is, they are not here to help us rollout a feature, diff --git a/WooCommerce/Classes/System/WooConstants.swift b/WooCommerce/Classes/System/WooConstants.swift index f1f3986e7bd..eaec33c12c8 100644 --- a/WooCommerce/Classes/System/WooConstants.swift +++ b/WooCommerce/Classes/System/WooConstants.swift @@ -154,9 +154,12 @@ extension WooConstants { /// URL for in-app feedback survey /// #if DEBUG - case inAppFeedback = "https://automattic.survey.fm/woo-app-general-feedback-test-survey" + case generalFeedback = "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 generalFeedback = "https://automattic.survey.fm/woo-app-general-feedback-user-survey" + // TODO: Create the production 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/Tools/Notices/DefaultNoticePresenter.swift b/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift index d8e8938be04..d03017f3dfc 100644 --- a/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift +++ b/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift @@ -30,6 +30,10 @@ class DefaultNoticePresenter: NoticePresenter { /// private var keyboardFrameObserver: KeyboardFrameObserver? + /// Notices are dismissed automatically by default, unless set otherwise + /// + var shouldDismissAutomatically: Bool? = true + /// Enqueues the specified Notice for display. /// @discardableResult @@ -171,7 +175,12 @@ private extension DefaultNoticePresenter { } animatePresentation(fromState: offScreenState, toState: onScreenState, completion: { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Animations.dismissDelay, execute: dismiss) + guard let shouldDismissAutomatically = self.shouldDismissAutomatically else { + return + } + if shouldDismissAutomatically { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Animations.dismissDelay, execute: dismiss) + } }) } diff --git a/WooCommerce/Classes/Tools/Notices/Notice.swift b/WooCommerce/Classes/Tools/Notices/Notice.swift index 904d32470a0..e896d681876 100644 --- a/WooCommerce/Classes/Tools/Notices/Notice.swift +++ b/WooCommerce/Classes/Tools/Notices/Notice.swift @@ -31,11 +31,14 @@ struct Notice { /// let actionTitle: String? + /// An optional handler closure that will be called when the notice is tapped + /// + var noticeTappedHandler: (() -> Void)? + /// An optional handler closure that will be called when the action button is tapped, if you've provided an action title /// let actionHandler: (() -> Void)? - /// Designated Initializer /// init(title: String, @@ -44,6 +47,7 @@ struct Notice { feedbackType: UINotificationFeedbackGenerator.FeedbackType? = nil, notificationInfo: NoticeNotificationInfo? = nil, actionTitle: String? = nil, + noticeTappedHandler: ((() -> Void))? = nil, actionHandler: ((() -> Void))? = nil) { self.title = title self.subtitle = subtitle @@ -51,6 +55,7 @@ struct Notice { self.feedbackType = feedbackType self.notificationInfo = notificationInfo self.actionTitle = actionTitle + self.noticeTappedHandler = noticeTappedHandler self.actionHandler = actionHandler } } diff --git a/WooCommerce/Classes/Tools/Notices/NoticeView.swift b/WooCommerce/Classes/Tools/Notices/NoticeView.swift index 61b2049b1cb..b569be08104 100644 --- a/WooCommerce/Classes/Tools/Notices/NoticeView.swift +++ b/WooCommerce/Classes/Tools/Notices/NoticeView.swift @@ -167,6 +167,8 @@ private extension NoticeView { if let subtitle = notice.subtitle { subtitleLabel.isHidden = false subtitleLabel.text = subtitle + // TODO: Remove color, just for testing: + subtitleLabel.textColor = .red } else { subtitleLabel.isHidden = true } @@ -193,6 +195,7 @@ private extension NoticeView { private extension NoticeView { @objc private func viewTapped() { + notice.noticeTappedHandler?() dismissHandler?() } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift index 57145d02737..ed2eb671b4a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift @@ -75,6 +75,9 @@ final class InPersonPaymentsMenuViewController: UIViewController { configureTableReload() runCardPresentPaymentsOnboarding() configureWebViewPresentation() + if featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) { + createIPPFeedbackNoticeBanner() + } viewModel.viewDidLoad() } } @@ -334,6 +337,32 @@ private extension InPersonPaymentsMenuViewController { self.navigationController?.show(connectionController, sender: nil) }.store(in: &cancellables) } + + private func createIPPFeedbackNoticeBanner() { + let notice = Notice( + title: Localization.feedbackNoticeBannerTitle, + subtitle: Localization.shareFeedbackNoticeBannerButton, + actionTitle: "x", // Experimenting with dismiss. + noticeTappedHandler: { + print("Notice tapped") + self.displayFeedbackSurvey() + }, + actionHandler: { + print("Dismiss tapped") + } + ) + + let noticePresenter = DefaultNoticePresenter() + noticePresenter.presentingViewController = self + noticePresenter.shouldDismissAutomatically = false + noticePresenter.enqueue(notice: notice) + } + + private func displayFeedbackSurvey() { + // TODO: Different surveys will be shown: + let surveyNavigation = SurveyCoordinatingController(survey: .IPPFeedback) + self.present(surveyNavigation, animated: true, completion: nil) + } } // MARK: - Convenience methods @@ -525,6 +554,16 @@ private extension InPersonPaymentsMenuViewController { "Continue setup", comment: "Call to Action to finish the setup of In-Person Payments in the Menu" ) + + static let feedbackNoticeBannerTitle = NSLocalizedString( + "Do you sell in person?", + comment: "Title of the feedback notice banner in the Payments tab" + ) + + static let shareFeedbackNoticeBannerButton = NSLocalizedString( + "Share Feedback", + comment: "Title of the feedback action button on the feedback notice banner" + ) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index 3a1f566e3a0..916d2a10f5f 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -376,7 +376,7 @@ private extension SettingsViewController { } func presentSurveyForFeedback() { - let surveyNavigation = SurveyCoordinatingController(survey: .inAppFeedback) + let surveyNavigation = SurveyCoordinatingController(survey: .generalFeedback) present(surveyNavigation, animated: true, completion: nil) } diff --git a/WooCommerce/Classes/ViewRelated/InAppFeedback/InAppFeedbackCardViewController.swift b/WooCommerce/Classes/ViewRelated/InAppFeedback/InAppFeedbackCardViewController.swift index ade7ddcf075..84655ac33fc 100644 --- a/WooCommerce/Classes/ViewRelated/InAppFeedback/InAppFeedbackCardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/InAppFeedback/InAppFeedbackCardViewController.swift @@ -77,7 +77,7 @@ private extension InAppFeedbackCardViewController { return } - let surveyNavigation = SurveyCoordinatingController(survey: .inAppFeedback) + let surveyNavigation = SurveyCoordinatingController(survey: .generalFeedback) self.present(surveyNavigation, animated: true, completion: nil) self.onFeedbackGiven?() self.analytics.track(event: .appFeedbackPrompt(action: .didntLike)) diff --git a/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift b/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift index 96fac6c4f03..e7a5ea6deae 100644 --- a/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Survey/SurveyViewController.swift @@ -62,17 +62,18 @@ final class SurveyViewController: UIViewController, SurveyViewControllerOutputs // extension SurveyViewController { enum Source { - case inAppFeedback + case generalFeedback case productsFeedback case shippingLabelsRelease3Feedback case addOnsI1 case orderCreation case couponManagement + case IPPFeedback fileprivate var url: URL { switch self { - case .inAppFeedback: - return WooConstants.URLs.inAppFeedback + case .generalFeedback: + return WooConstants.URLs.generalFeedback .asURL() .tagPlatform("ios") .tagAppVersion(Bundle.main.bundleVersion()) @@ -102,14 +103,19 @@ extension SurveyViewController { .asURL() .tagPlatform("ios") .tagAppVersion(Bundle.main.bundleVersion()) + case .IPPFeedback: + return WooConstants.URLs.IPPFeedback + .asURL() + .tagPlatform("ios") + .tagAppVersion(Bundle.main.bundleVersion()) } } fileprivate var title: String { switch self { - case .inAppFeedback: + case .generalFeedback: 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 .generalFeedback, .IPPFeedback: return .general case .productsFeedback: return .productsGeneral diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyCoordinatorControllerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyCoordinatorControllerTests.swift index b99a2fff062..80a48e7f0bc 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyCoordinatorControllerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyCoordinatorControllerTests.swift @@ -28,7 +28,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { let factory = MockSurveyViewControllersFactory() // When - let coordinator = SurveyCoordinatingController(survey: .inAppFeedback, viewControllersFactory: factory) + let coordinator = SurveyCoordinatingController(survey: .generalFeedback, viewControllersFactory: factory) // Then XCTAssertTrue(coordinator.topViewController is SurveyViewControllerOutputs) @@ -37,7 +37,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { func test_it_navigates_to_SurveySubmittedViewController_when_survey_is_submitted() throws { // Given let factory = MockSurveyViewControllersFactory() - let coordinator = SurveyCoordinatingController(survey: .inAppFeedback, viewControllersFactory: factory) + let coordinator = SurveyCoordinatingController(survey: .generalFeedback, viewControllersFactory: factory) // When factory.surveyViewController.onCompletion() @@ -51,7 +51,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { func test_it_gets_dismissed_on_backToStore_action() throws { // Given let factory = MockSurveyViewControllersFactory() - let coordinator = SurveyCoordinatingController(survey: .inAppFeedback, viewControllersFactory: factory) + let coordinator = SurveyCoordinatingController(survey: .generalFeedback, viewControllersFactory: factory) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() @@ -72,7 +72,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { // Given let zendeskManager = MockZendeskManager() let factory = MockSurveyViewControllersFactory() - let coordinator = SurveyCoordinatingController(survey: .inAppFeedback, zendeskManager: zendeskManager, viewControllersFactory: factory) + let coordinator = SurveyCoordinatingController(survey: .generalFeedback, zendeskManager: zendeskManager, viewControllersFactory: factory) assertEmpty(zendeskManager.newRequestIfPossibleInvocations) // When @@ -90,7 +90,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { func test_it_tracks_a_surveyScreen_completed_event_when_the_survey_is_submitted() throws { // Given let factory = MockSurveyViewControllersFactory() - _ = SurveyCoordinatingController(survey: .inAppFeedback, + _ = SurveyCoordinatingController(survey: .generalFeedback, viewControllersFactory: factory, analytics: analytics) @@ -113,7 +113,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { let factory = MockSurveyViewControllersFactory() // When - _ = SurveyCoordinatingController(survey: .inAppFeedback, + _ = SurveyCoordinatingController(survey: .generalFeedback, viewControllersFactory: factory, analytics: analytics) @@ -138,7 +138,7 @@ final class SurveyCoordinatingControllerTests: XCTestCase { window.rootViewController = rootViewController window.isHidden = false - let coordinator = SurveyCoordinatingController(survey: .inAppFeedback, + let coordinator = SurveyCoordinatingController(survey: .generalFeedback, viewControllersFactory: factory, analytics: analytics) waitForExpectation { exp in diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyViewControllerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyViewControllerTests.swift index 54ece83d22b..b71119f0808 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyViewControllerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Survey/SurveyViewControllerTests.swift @@ -11,7 +11,7 @@ final class SurveyViewControllerTests: XCTestCase { func test_it_loads_the_correct_inApp_feedback_survey() throws { // Given - let viewController = SurveyViewController(survey: .inAppFeedback, onCompletion: {}) + let viewController = SurveyViewController(survey: .generalFeedback, onCompletion: {}) // When _ = try XCTUnwrap(viewController.view) @@ -56,7 +56,7 @@ final class SurveyViewControllerTests: XCTestCase { func test_it_completes_after_receiving_a_form_submitted_completed_callback_request() throws { // Given var surveyCompleted = false - let viewController = SurveyViewController(survey: .inAppFeedback, onCompletion: { + let viewController = SurveyViewController(survey: .generalFeedback, onCompletion: { surveyCompleted = true }) @@ -77,7 +77,7 @@ final class SurveyViewControllerTests: XCTestCase { func test_it_does_not_complete_after_receiving_a_form_submitted_non_completed_callback_request() throws { // Given var surveyCompleted = false - let viewController = SurveyViewController(survey: .inAppFeedback, onCompletion: { + let viewController = SurveyViewController(survey: .generalFeedback, onCompletion: { surveyCompleted = true }) @@ -100,7 +100,7 @@ final class SurveyViewControllerTests: XCTestCase { func test_it_does_not_complete_after_receiving_a_form_submitted_empty_callback_request() throws { // Given var surveyCompleted = false - let viewController = SurveyViewController(survey: .inAppFeedback, onCompletion: { + let viewController = SurveyViewController(survey: .generalFeedback, onCompletion: { surveyCompleted = true }) @@ -122,7 +122,7 @@ final class SurveyViewControllerTests: XCTestCase { func test_it_shows_the_loading_view_when_loading_a_survey() throws { // Given - let viewController = SurveyViewController(survey: .inAppFeedback, onCompletion: {}) + let viewController = SurveyViewController(survey: .generalFeedback, onCompletion: {}) // When _ = try XCTUnwrap(viewController.view) @@ -134,7 +134,7 @@ final class SurveyViewControllerTests: XCTestCase { func test_it_hides_the_loading_view_after_loading_a_survey() throws { // Given - let viewController = SurveyViewController(survey: .inAppFeedback, onCompletion: {}) + let viewController = SurveyViewController(survey: .generalFeedback, onCompletion: {}) // When _ = try XCTUnwrap(viewController.view)