Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Storage/Storage/Model/FeedbackType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions WooCommerce/Classes/System/WooConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ private extension OrderListViewController {
self.setErrorTopBanner()
case .orderCreation:
self.setOrderCreationTopBanner()
case .IPPFeedback:
self.setIPPFeedbackTopBanner()
}
}
.store(in: &cancellables)
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down
52 changes: 44 additions & 8 deletions WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -165,10 +174,12 @@ final class OrderListViewModel {

observeForegroundRemoteNotifications()
bindTopBannerState()
loadOrdersBannerVisibility()

if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) {
fetchIPPTransactions()
syncIPPBannerVisibility()
loadOrdersBannerVisibility()
} else {
loadOrdersBannerVisibility()
}
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So beautiful code 😉

return .error
}

if hasDismissedOrdersBanners {
return .none
guard hasDismissedIPPFeedbackBanner else {
return .IPPFeedback
}

return .orderCreation
return hasDismissedOrdersBanners ? .none : .orderCreation
}
.assign(to: &$topBanner)
}
Expand Down Expand Up @@ -419,6 +454,7 @@ extension OrderListViewModel {
enum TopBanner {
case error
case orderCreation
case IPPFeedback
case none
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ extension SurveyViewController {
case addOnsI1
case orderCreation
case couponManagement
case IPPFeedback

fileprivate var url: URL {
switch self {
Expand Down Expand Up @@ -102,22 +103,27 @@ 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:
return Localization.title
case .productsFeedback, .shippingLabelsRelease3Feedback, .addOnsI1, .orderCreation, .couponManagement:
case .productsFeedback, .shippingLabelsRelease3Feedback, .addOnsI1, .orderCreation, .couponManagement, .IPPFeedback:
return Localization.giveFeedback
}
}

/// The corresponding `FeedbackContext` for event tracking purposes.
var feedbackContextForEvents: WooAnalyticsEvent.FeedbackContext {
switch self {
case .inAppFeedback:
case .inAppFeedback, .IPPFeedback:
return .general
case .productsFeedback:
return .productsGeneral
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -253,13 +253,36 @@ final class OrderListViewModelTests: XCTestCase {

// When
viewModel.activate()
viewModel.hideIPPFeedbackBanner = true

// Then
waitUntil {
viewModel.topBanner == .none
}
}

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)
Expand All @@ -274,13 +297,44 @@ final class OrderListViewModelTests: XCTestCase {

// When
viewModel.activate()
viewModel.hideIPPFeedbackBanner = true

// Then
waitUntil {
viewModel.topBanner == .orderCreation
}
}

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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down