diff --git a/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift b/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift index 7bb10bc0dfd..e864346fa21 100644 --- a/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift +++ b/Storage/Storage/Model/Feature Announcements/FeatureAnnouncementCampaign.swift @@ -4,6 +4,7 @@ public enum FeatureAnnouncementCampaign: String, Codable, Equatable { case upsellCardReaders = "upsell_card_readers" case linkedProductsPromo = "linked_products_promo" case productsOnboarding = "products_onboarding_first_product" + case IPP = "IPP_feedback_request" /// 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 181a3e58a80..ef0e89a7d8c 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/FeatureAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/FeatureAnnouncementCardViewModel.swift @@ -106,8 +106,9 @@ class FeatureAnnouncementCardViewModel: AnnouncementCardViewModelProtocol { } private func storeDismissedSetting(remindLater: Bool) { + let remindAfterDays = remindLater ? 0 : nil let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: config.campaign, - remindLater: remindLater, + remindAfterDays: remindAfterDays, onCompletion: nil) stores.dispatch(action) shouldBeVisible = false diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift index 290f8976dc7..1ff2b9f3c72 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/ProductsOnboardingAnnouncementCardViewModel.swift @@ -34,7 +34,7 @@ struct ProductsOnboardingAnnouncementCardViewModel: AnnouncementCardViewModelPro /// func dontShowAgainTapped() { let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .productsOnboarding, - remindLater: false, + remindAfterDays: nil, onCompletion: nil) ServiceLocator.stores.dispatch(action) } diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift index c915d4d8654..5d0417b2023 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift @@ -802,8 +802,10 @@ private extension OrderListViewController { } private func createIPPFeedbackTopBanner(survey: SurveyViewController.Source) -> TopBannerView { - let shareIPPFeedbackAction = TopBannerViewModel.ActionButton(title: Localization.shareFeedbackButton, action: { _ in - self.displayIPPFeedbackBannerSurvey(survey: survey) + let shareIPPFeedbackAction = TopBannerViewModel.ActionButton(title: Localization.shareFeedbackButton, action: { [weak self] _ in + self?.displayIPPFeedbackBannerSurvey(survey: survey) + // We dismiss the banner at this point as we cannot know if the user successfully submitted it + self?.viewModel.IPPFeedbackBannerWasDismissed() }) var bannerTitle = "" @@ -828,7 +830,9 @@ private extension OrderListViewController { infoText: bannerText, icon: UIImage.gridicon(.comment), isExpanded: true, - topButton: .dismiss(handler: { }), + topButton: .dismiss(handler: { + self.showIPPFeedbackDismissAlert() + }), actionButtons: [shareIPPFeedbackAction] ) let topBannerView = TopBannerView(viewModel: viewModel) @@ -840,6 +844,26 @@ private extension OrderListViewController { let surveyNavigation = SurveyCoordinatingController(survey: survey) self.present(surveyNavigation, animated: true, completion: nil) } + + private func showIPPFeedbackDismissAlert() { + let actionSheet = UIAlertController( + title: Localization.dismissTitle, + message: Localization.dismissMessage, + preferredStyle: .alert + ) + + let remindMeLaterAction = UIAlertAction( title: Localization.remindMeLater, style: .default) { [weak self] _ in + self?.viewModel.IPPFeedbackBannerRemindMeLaterTapped() + } + actionSheet.addAction(remindMeLaterAction) + + let dontShowAgainAction = UIAlertAction( title: Localization.dontShowAgain, style: .default) { [weak self] _ in + self?.viewModel.IPPFeedbackBannerDontShowAgainTapped() + } + actionSheet.addAction(dontShowAgainAction) + + self.present(actionSheet, animated: true) + } } // MARK: - Constants @@ -886,6 +910,21 @@ private extension OrderListViewController { comment: "Title of the feedback action button on the In-Person Payments feedback banner" ) + static let dismissTitle = NSLocalizedString("Give feedback", + comment: "Title of the modal confirmation screen when the In-Person Payments feedback banner is dismissed" + ) + + static let dismissMessage = NSLocalizedString("No worries! You can always go to Settings in the Menu to send us feedback.", + comment: "Message of the modal confirmation screen when the In-Person Payments feedback banner is dismissed") + + static let remindMeLater = NSLocalizedString("Remind me later", + comment: "Title of the button shown when the In-Person Payments feedback banner is dismissed." + ) + + static let dontShowAgain = NSLocalizedString("Don't show again", + comment: "Title of the button shown when the In-Person Payments feedback banner is dismissed." + ) + 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 f2a5692cc6e..ff81bbe64bb 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -346,6 +346,38 @@ final class OrderListViewModel { } } +// MARK: - In-Person Payments Feedback Banner + +extension OrderListViewModel { + func dismissIPPFeedbackBanner(remindAfterDays: Int?) { + // Updates the IPP feedback banner status as dismissed + let updateFeedbackStatus = AppSettingsAction.updateFeedbackStatus(type: .IPP, status: .dismissed) { [weak self] _ in + self?.hideIPPFeedbackBanner = true + } + stores.dispatch(updateFeedbackStatus) + + // Updates the IPP feedback banner status to be reminded later, or never + let updateBannerVisibility = AppSettingsAction.setFeatureAnnouncementDismissed( + campaign: .IPP, + remindAfterDays: remindAfterDays, + onCompletion: nil + ) + stores.dispatch(updateBannerVisibility) + } + + func IPPFeedbackBannerRemindMeLaterTapped() { + dismissIPPFeedbackBanner(remindAfterDays: Constants.remindIPPBannerDismissalAfterDays) + } + + func IPPFeedbackBannerDontShowAgainTapped() { + dismissIPPFeedbackBanner(remindAfterDays: nil) + } + + func IPPFeedbackBannerWasDismissed() { + dismissIPPFeedbackBanner(remindAfterDays: nil) + } +} + // MARK: - Remote Notifications Observation private extension OrderListViewModel { @@ -461,5 +493,6 @@ private extension OrderListViewModel { static let paymentMethodTitle = "WooCommerce In-Person Payments" static let receiptURLKey = "receipt_url" static let numberOfTransactions = 10 + static let remindIPPBannerDismissalAfterDays = 7 } } diff --git a/Yosemite/Yosemite/Actions/AppSettingsAction.swift b/Yosemite/Yosemite/Actions/AppSettingsAction.swift index 97324c21796..e9f0a7b1a9b 100644 --- a/Yosemite/Yosemite/Actions/AppSettingsAction.swift +++ b/Yosemite/Yosemite/Actions/AppSettingsAction.swift @@ -197,7 +197,11 @@ public enum AppSettingsAction: Action { // MARK: - Feature Announcement Card Visibility - case setFeatureAnnouncementDismissed(campaign: FeatureAnnouncementCampaign, remindLater: Bool, onCompletion: ((Result) -> ())?) + case setFeatureAnnouncementDismissed( + campaign: FeatureAnnouncementCampaign, + remindAfterDays: Int?, + onCompletion: ((Result) -> ())? + ) case getFeatureAnnouncementVisibility(campaign: FeatureAnnouncementCampaign, onCompletion: (Result) -> ()) diff --git a/Yosemite/Yosemite/Stores/AppSettingsStore.swift b/Yosemite/Yosemite/Stores/AppSettingsStore.swift index 6d63ede01a5..400bf7f7e5a 100644 --- a/Yosemite/Yosemite/Stores/AppSettingsStore.swift +++ b/Yosemite/Yosemite/Stores/AppSettingsStore.swift @@ -178,8 +178,8 @@ public class AppSettingsStore: Store { setCouponManagementFeatureSwitchState(isEnabled: isEnabled, onCompletion: onCompletion) case .loadCouponManagementFeatureSwitchState(let onCompletion): loadCouponManagementFeatureSwitchState(onCompletion: onCompletion) - case .setFeatureAnnouncementDismissed(campaign: let campaign, remindLater: let remindLater, onCompletion: let completion): - setFeatureAnnouncementDismissed(campaign: campaign, remindLater: remindLater, onCompletion: completion) + case .setFeatureAnnouncementDismissed(campaign: let campaign, remindAfterDays: let remindAfterDays, onCompletion: let completion): + setFeatureAnnouncementDismissed(campaign: campaign, remindAfterDays: remindAfterDays, onCompletion: completion) case .getFeatureAnnouncementVisibility(campaign: let campaign, onCompletion: let completion): getFeatureAnnouncementVisibility(campaign: campaign, onCompletion: completion) case .setSkippedCashOnDeliveryOnboardingStep(siteID: let siteID): @@ -758,20 +758,26 @@ private extension AppSettingsStore { extension AppSettingsStore { - func setFeatureAnnouncementDismissed(campaign: FeatureAnnouncementCampaign, remindLater: Bool, onCompletion: ((Result) -> ())?) { - do { - let remindAfter = remindLater ? Date().addingDays(14) : nil - let newSettings = FeatureAnnouncementCampaignSettings(dismissedDate: Date(), remindAfter: remindAfter) - - let settings = generalAppSettings.settings - let settingsToSave = settings.replacing(featureAnnouncementSettings: newSettings, for: campaign) - try generalAppSettings.saveSettings(settingsToSave) - - onCompletion?(.success(true)) - } catch { - onCompletion?(.failure(error)) + func setFeatureAnnouncementDismissed( + campaign: FeatureAnnouncementCampaign, + remindAfterDays: Int?, + onCompletion: ((Result) -> ())?) { + do { + guard let remindAfterDays else { + return + } + let remindAfter = Date().addingDays(remindAfterDays) + let newSettings = FeatureAnnouncementCampaignSettings(dismissedDate: Date(), remindAfter: remindAfter) + + let settings = generalAppSettings.settings + let settingsToSave = settings.replacing(featureAnnouncementSettings: newSettings, for: campaign) + try generalAppSettings.saveSettings(settingsToSave) + + onCompletion?(.success(true)) + } catch { + onCompletion?(.failure(error)) + } } - } func getFeatureAnnouncementVisibility(campaign: FeatureAnnouncementCampaign, onCompletion: (Result) -> ()) { guard let campaignSettings = generalAppSettings.value(for: \.featureAnnouncementCampaignSettings)[campaign] else { diff --git a/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift b/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift index 719bfee43ce..fb1bce7689c 100644 --- a/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift @@ -841,6 +841,23 @@ final class AppSettingsStoreTests: XCTestCase { extension AppSettingsStoreTests { + func test_setFeatureAnnouncementDismissed_for_campaign_when_remindAfterDays_is_nil_then_does_not_store_current_date() throws { + // Given + try fileStorage?.deleteFile(at: expectedGeneralStoreSettingsFileURL) + // When + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindAfterDays: nil, onCompletion: nil) + subject?.onAction(action) + + // Then + var savedSettings: GeneralAppSettings? = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) + XCTAssertNil(savedSettings) + guard let savedSettings else { + return + } + var savedDate: Date? = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) + XCTAssertNil(savedDate) + } + func test_setFeatureAnnouncementDismissed_for_campaign_stores_current_date() throws { // Given let currentTime = Date() @@ -848,7 +865,7 @@ extension AppSettingsStoreTests { try fileStorage?.deleteFile(at: expectedGeneralStoreSettingsFileURL) // When - let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindLater: false, onCompletion: nil) + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindAfterDays: 0, onCompletion: nil) subject?.onAction(action) // Then @@ -859,14 +876,15 @@ extension AppSettingsStoreTests { XCTAssert(Calendar.current.isDate(actualDismissDate, inSameDayAs: currentTime)) } - func test_setFeatureAnnouncementDismissed_with_remindLater_true_stores_reminder_date_in_two_weeks() throws { + func test_setFeatureAnnouncementDismissed_when_remindAfterDays_is_two_weeks_then_stores_reminder_date_is_two_weeks() throws { // Given - let twoWeeksTime = Calendar.current.date(byAdding: .day, value: 14, to: Date())! + let remindAfterDays = 14 + let twoWeeksTime = Calendar.current.date(byAdding: .day, value: remindAfterDays, to: Date())! try fileStorage?.deleteFile(at: expectedGeneralStoreSettingsFileURL) // When - let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindLater: true, onCompletion: nil) + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindAfterDays: remindAfterDays, onCompletion: nil) subject?.onAction(action) // Then @@ -877,18 +895,38 @@ extension AppSettingsStoreTests { XCTAssert(Calendar.current.isDate(actualRemindAfter, inSameDayAs: twoWeeksTime)) } + func test_setFeatureAnnouncementDismissed_when_remindAfterDays_is_seven_days_stores_reminder_then_date_saved_date_is_one_week() throws { + // Given + let remindAfterDays = 7 + let oneWeekTime = Calendar.current.date(byAdding: .day, value: remindAfterDays, to: Date())! + + try fileStorage?.deleteFile(at: expectedGeneralStoreSettingsFileURL) + + // When + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindAfterDays: remindAfterDays, onCompletion: nil) + subject?.onAction(action) + + // Then + let savedSettings: GeneralAppSettings = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) + + let actualRemindAfter = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.remindAfter) + + XCTAssert(Calendar.current.isDate(actualRemindAfter, inSameDayAs: oneWeekTime)) + } + func test_setFeatureAnnouncementDismissed_with_another_campaign_previously_dismissed_keeps_values_for_both() throws { // Given try fileStorage?.deleteFile(at: expectedGeneralStoreSettingsFileURL) let currentTime = Date() - let date = Date(timeIntervalSince1970: 100) + let datePrior = Date(timeIntervalSince1970: 100) + let date = Date() let settings = createAppSettings(featureAnnouncementCampaignSettings: [.test: .init(dismissedDate: date, remindAfter: nil)]) try fileStorage?.write(settings, to: expectedGeneralAppSettingsFileURL) // When - let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindLater: false, onCompletion: nil) + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .upsellCardReaders, remindAfterDays: 0, onCompletion: nil) subject?.onAction(action) // Then