diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index e27988eb87e..d8d83cb6fca 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -87,8 +87,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return buildConfig == .localDeveloper || buildConfig == .alpha case .productGlobalUniqueIdentifierSupport: return true - case .sendReceiptsForPointOfSale: - return true case .hideSitesInStorePicker: return true case .filterHistoryOnOrderAndProductLists: diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 5801658293b..05345af972c 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -189,10 +189,6 @@ public enum FeatureFlag: Int { /// case productGlobalUniqueIdentifierSupport - /// Adds support for sending receipts after the payment for POS - /// - case sendReceiptsForPointOfSale - /// Supports hiding sites from the store picker /// case hideSitesInStorePicker diff --git a/WooCommerce/Classes/Analytics/TracksProvider.swift b/WooCommerce/Classes/Analytics/TracksProvider.swift index 24c7599d6b7..3dc7f996ad0 100644 --- a/WooCommerce/Classes/Analytics/TracksProvider.swift +++ b/WooCommerce/Classes/Analytics/TracksProvider.swift @@ -131,8 +131,7 @@ private extension TracksProvider { WooAnalyticsStat.pointOfSaleGetSupportTapped, WooAnalyticsStat.pointOfSaleSimpleProductsExplanationDialogShown, WooAnalyticsStat.pointOfSaleCreateNewOrderTapped, - WooAnalyticsStat.pointOfSaleEmailReceiptTapped, - WooAnalyticsStat.pointOfSaleEmailReceiptSendTapped, + WooAnalyticsStat.pointOfSaleReceiptEmailSendTapped, WooAnalyticsStat.pointOfSalePaymentsOnboardingShown, WooAnalyticsStat.pointOfSalePaymentsOnboardingDismissed, WooAnalyticsStat.pointOfSaleCardReaderConnectionTapped, @@ -171,7 +170,9 @@ private extension TracksProvider { WooAnalyticsStat.cardPresentOnboardingCtaFailed, // Receipts - // TODO: 15058-gh + WooAnalyticsStat.receiptEmailTapped, + WooAnalyticsStat.receiptEmailSuccess, + WooAnalyticsStat.receiptEmailFailed, // Payments WooAnalyticsStat.collectPaymentCanceled, diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index 66830ed43bf..da23139f130 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1283,8 +1283,7 @@ enum WooAnalyticsStat: String { case pointOfSaleGetSupportTapped = "get_support_tapped" case pointOfSaleSimpleProductsExplanationDialogShown = "simple_products_explanation_dialog_shown" case pointOfSaleCreateNewOrderTapped = "create_new_order_tapped" - case pointOfSaleEmailReceiptTapped = "email_receipt_tapped" - case pointOfSaleEmailReceiptSendTapped = "email_receipt_send_tapped" + case pointOfSaleReceiptEmailSendTapped = "receipt_email_send_tapped" case pointOfSalePaymentsOnboardingShown = "payments_onboarding_shown" case pointOfSalePaymentsOnboardingDismissed = "payments_onboarding_dismissed" case pointOfSaleCardReaderConnectionTapped = "card_reader_connection_tapped" diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 08600840dc5..994fb88cfd3 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -278,7 +278,14 @@ extension PointOfSaleAggregateModel { @MainActor func sendReceipt(to emailAddress: String) async throws { - try await orderController.sendReceipt(recipientEmail: emailAddress) + do { + try await orderController.sendReceipt(recipientEmail: emailAddress) + analytics.track(.receiptEmailSuccess) + } catch { + // Catch and re-throw in order to capture the error event, but still allow the UI to handle the error state. + analytics.track(.receiptEmailFailed) + throw error + } } @MainActor diff --git a/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift b/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift index 62edd70df11..f5998f17daa 100644 --- a/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift +++ b/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift @@ -8,16 +8,11 @@ struct PaymentsActionButtons: View { private let receiptEligibilityUseCase = ReceiptEligibilityUseCase() - private var shouldShowSendReceiptButton: Bool { - ServiceLocator.featureFlagService.isFeatureFlagEnabled(.sendReceiptsForPointOfSale) - } - var body: some View { ZStack { VStack { newOrderButton sendReceiptButton - .renderedIf(shouldShowSendReceiptButton) } } } @@ -28,7 +23,7 @@ private extension PaymentsActionButtons { var sendReceiptButton: some View { Button(action: { Task { @MainActor in - ServiceLocator.analytics.track(.pointOfSaleEmailReceiptTapped) + ServiceLocator.analytics.track(.receiptEmailTapped) await handleSendReceiptAction() } }, label: { diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift index e0d12765501..2d37c886a2d 100644 --- a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift @@ -73,7 +73,7 @@ struct POSSendReceiptView: View { } private func sendReceipt() { - ServiceLocator.analytics.track(.pointOfSaleEmailReceiptSendTapped) + ServiceLocator.analytics.track(.pointOfSaleReceiptEmailSendTapped) Task { @MainActor in guard isEmailValid else { errorMessage = Localization.emailValidationErrorText diff --git a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift index 155ce9da07b..14a0df43b64 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift @@ -44,11 +44,6 @@ final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { /// WooCommerce 9.5 allows to attach a customer email after payment is made and send email receipt via the API. /// func isEligibleForPointOfSaleReceipts(onCompletion: @escaping (Bool) -> Void) { - guard featureFlagService.isFeatureFlagEnabled(.sendReceiptsForPointOfSale) else { - onCompletion(false) - return - } - Task { @MainActor in let isWooCommerceSupported = await isPluginSupported(Constants.wcPluginName, minimumVersion: Constants.PointOfSaleReceipts.wcPluginMinimumVersion) diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index 3058b6cb69d..04ae3185a36 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -22,7 +22,6 @@ final class MockFeatureFlagService: FeatureFlagService { var viewEditCustomFieldsInProductsAndOrders: Bool var favoriteProducts: Bool var isProductGlobalUniqueIdentifierSupported: Bool - var receiptsForPOS: Bool var hideSitesInStorePicker: Bool init(isInboxOn: Bool = false, @@ -45,7 +44,6 @@ final class MockFeatureFlagService: FeatureFlagService { viewEditCustomFieldsInProductsAndOrders: Bool = false, favoriteProducts: Bool = false, isProductGlobalUniqueIdentifierSupported: Bool = false, - receiptsForPOS: Bool = false, hideSitesInStorePicker: Bool = false) { self.isInboxOn = isInboxOn self.isShowInboxCTAEnabled = isShowInboxCTAEnabled @@ -67,7 +65,6 @@ final class MockFeatureFlagService: FeatureFlagService { self.viewEditCustomFieldsInProductsAndOrders = viewEditCustomFieldsInProductsAndOrders self.favoriteProducts = favoriteProducts self.isProductGlobalUniqueIdentifierSupported = isProductGlobalUniqueIdentifierSupported - self.receiptsForPOS = receiptsForPOS self.hideSitesInStorePicker = hideSitesInStorePicker } @@ -113,8 +110,6 @@ final class MockFeatureFlagService: FeatureFlagService { return favoriteProducts case .productGlobalUniqueIdentifierSupport: return isProductGlobalUniqueIdentifierSupported - case .sendReceiptsForPointOfSale: - return receiptsForPOS case .hideSitesInStorePicker: return hideSitesInStorePicker default: diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift index d1d06202bf6..93a282d17fb 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift @@ -41,5 +41,12 @@ final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol { clearOrderWasCalled = true } - func sendReceipt(recipientEmail: String) async { } + var shouldThrowReceiptError: Bool = false + var sendReceiptWasCalled: Bool = false + func sendReceipt(recipientEmail: String) async throws { + sendReceiptWasCalled = true + if shouldThrowReceiptError { + throw NSError(domain: "some error", code: -1) + } + } } diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index bee9b2e27da..7a3acf0f739 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -298,6 +298,45 @@ struct PointOfSaleAggregateModelTests { // Then #expect(cardPresentPaymentService.collectPaymentChannel == .pos) } + + @available(iOS 17.0, *) + @Test func sendReceipt_when_invoked_then_calls_controller() async throws { + // Given + let orderController = MockPointOfSaleOrderController() + let sut = PointOfSaleAggregateModel(itemsController: MockPointOfSaleItemsController(), + cardPresentPaymentService: MockCardPresentPaymentService(), + orderController: orderController, + collectOrderPaymentAnalyticsTracker: MockCollectOrderPaymentAnalyticsTracker()) + + // When + try await sut.sendReceipt(to: "") + + // Then + #expect(orderController.sendReceiptWasCalled == true) + } + + @available(iOS 17.0, *) + @Test func sendReceipt_when_invoked_with_error_then_returns_error() async throws { + // Given + let orderController = MockPointOfSaleOrderController() + orderController.shouldThrowReceiptError = true + let expectedError = NSError(domain: "some error", code: -1) + + let sut = PointOfSaleAggregateModel(itemsController: MockPointOfSaleItemsController(), + cardPresentPaymentService: MockCardPresentPaymentService(), + orderController: orderController, + collectOrderPaymentAnalyticsTracker: MockCollectOrderPaymentAnalyticsTracker()) + + do { + // When + try await sut.sendReceipt(to: "") + } catch { + // Then + let nsError = error as NSError + #expect(nsError.domain == expectedError.domain) + #expect(nsError.code == expectedError.code) + } + } } struct PaymentTests { diff --git a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift index fff39809c35..5a56c1b7632 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift @@ -88,64 +88,6 @@ final class ReceiptEligibilityUseCaseTests: XCTestCase { XCTAssertFalse(isEligible) } - func test_isEligibleForPOSReceipts_when_WooCommerce_version_is_correct_version_then_returns_true() { - // Given - let featureFlag = MockFeatureFlagService(receiptsForPOS: true) - let stores = MockStoresManager(sessionManager: .makeForTesting()) - let plugin = SystemPlugin.fake().copy(name: "WooCommerce", - version: "9.5", - active: true) - - stores.whenReceivingAction(ofType: SystemStatusAction.self) { action in - switch action { - case let .fetchSystemPlugin(_, _, onCompletion): - onCompletion(plugin) - default: - XCTFail("Unexpected action") - } - } - let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) - - // When - let isEligible: Bool = waitFor { promise in - sut.isEligibleForPointOfSaleReceipts(onCompletion: { result in - promise(result) - }) - } - - // Then - XCTAssertTrue(isEligible) - } - - func test_isEligibleForPOSReceipts_when_WooCommerce_version_is_incorrect_version_then_returns_false() { - // Given - let featureFlag = MockFeatureFlagService(receiptsForPOS: true) - let stores = MockStoresManager(sessionManager: .makeForTesting()) - let plugin = SystemPlugin.fake().copy(name: "WooCommerce", - version: "9.4", - active: true) - - stores.whenReceivingAction(ofType: SystemStatusAction.self) { action in - switch action { - case let .fetchSystemPlugin(_, _, onCompletion): - onCompletion(plugin) - default: - XCTFail("Unexpected action") - } - } - let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) - - // When - let isEligible: Bool = waitFor { promise in - sut.isEligibleForPointOfSaleReceipts(onCompletion: { result in - promise(result) - }) - } - - // Then - XCTAssertFalse(isEligible) - } - func test_when_WooCommerce_version_is_equal_or_above_minimum_then_returns_true() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting())