diff --git a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift index 7e8a9168bb6..12f7f3bb334 100644 --- a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift +++ b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift @@ -98,6 +98,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return buildConfig == .localDeveloper || buildConfig == .alpha case .ciabBookings: return buildConfig == .localDeveloper || buildConfig == .alpha + case .ciab: + return buildConfig == .localDeveloper || buildConfig == .alpha case .pointOfSaleSurveys: return true case .pointOfSaleCatalogAPI: diff --git a/Modules/Sources/Experiments/FeatureFlag.swift b/Modules/Sources/Experiments/FeatureFlag.swift index 54f04e701f5..9d3d410c4a1 100644 --- a/Modules/Sources/Experiments/FeatureFlag.swift +++ b/Modules/Sources/Experiments/FeatureFlag.swift @@ -204,6 +204,11 @@ public enum FeatureFlag: Int { /// case ciabBookings + /// Represents CIAB environment availability overall + /// Has same underlying logic as `ciabBookings` flag. + /// + case ciab + /// Enables surveys for potential and current POS merchants /// case pointOfSaleSurveys diff --git a/Modules/Sources/Yosemite/Stores/OrderCardPresentPaymentEligibilityStore.swift b/Modules/Sources/Yosemite/Stores/OrderCardPresentPaymentEligibilityStore.swift index 72b3c4ec90d..0e9ed868cf1 100644 --- a/Modules/Sources/Yosemite/Stores/OrderCardPresentPaymentEligibilityStore.swift +++ b/Modules/Sources/Yosemite/Stores/OrderCardPresentPaymentEligibilityStore.swift @@ -1,11 +1,15 @@ import Foundation import protocol Storage.StorageManagerType import protocol NetworkingCore.Network +import protocol WooFoundation.CrashLogger +import enum WooFoundation.SeverityLevel /// Determines whether an order is eligible for card present payment or not /// public final class OrderCardPresentPaymentEligibilityStore: Store { private let currentSite: () -> Site? + private let isCIABEnvironmentSupported: () -> Bool + private let crashLogger: CrashLogger private lazy var siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker( currentSite: currentSite ) @@ -14,9 +18,13 @@ public final class OrderCardPresentPaymentEligibilityStore: Store { dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network, + crashLogger: CrashLogger, + isCIABEnvironmentSupported: @escaping () -> Bool, currentSite: @escaping () -> Site? ) { self.currentSite = currentSite + self.isCIABEnvironmentSupported = isCIABEnvironmentSupported + self.crashLogger = crashLogger super.init( dispatcher: dispatcher, storageManager: storageManager, @@ -56,20 +64,40 @@ private extension OrderCardPresentPaymentEligibilityStore { onCompletion: (Result) -> Void) { let storage = storageManager.viewStorage - guard let site = storage.loadSite(siteID: siteID)?.toReadOnly() else { - return onCompletion( - .failure( - OrderIsEligibleForCardPresentPaymentError.siteNotFoundInStorage + /// The following checks are only relevant if CIAB is rolled out. + if isCIABEnvironmentSupported() { + let storageSite = storage.loadSite(siteID: siteID)?.toReadOnly() + + let site: Site? + if let storageSite { + site = storageSite + } else { + /// Non - fatal fallback to `currentSite` when a storage site is missing + site = currentSite() + + logFailedStorageSiteRead( + siteID: siteID, + currentSiteFallbackValue: site ) - ) - } + } - guard siteCIABEligibilityChecker.isFeatureSupported(.cardReader, for: site) else { - return onCompletion( - .failure( - OrderIsEligibleForCardPresentPaymentError.cardReaderPaymentOptionIsNotSupportedForCIABSites + guard let site else { + logFailedDefaultSiteRead(siteID: siteID) + + return onCompletion( + .failure( + OrderIsEligibleForCardPresentPaymentError.failedToObtainSite + ) ) - ) + } + + guard siteCIABEligibilityChecker.isFeatureSupported(.cardReader, for: site) else { + return onCompletion( + .failure( + OrderIsEligibleForCardPresentPaymentError.cardReaderPaymentOptionIsNotSupportedForCIABSites + ) + ) + } } guard let order = storage.loadOrder(siteID: siteID, orderID: orderID)?.toReadOnly() else { @@ -83,10 +111,42 @@ private extension OrderCardPresentPaymentEligibilityStore { } } +/// Error logging +private extension OrderCardPresentPaymentEligibilityStore { + func logFailedStorageSiteRead(siteID: Int64, currentSiteFallbackValue: Site?) { + let message = "OrderCardPresentPaymentEligibilityStore: Storage site missing, falling back to currentSite." + + DDLogError(message) + + crashLogger.logMessage( + message, + properties: [ + "siteID": siteID, + "currentSiteID": currentSiteFallbackValue?.siteID ?? "empty", + ], + level: .error + ) + } + + func logFailedDefaultSiteRead(siteID: Int64) { + let message = "OrderCardPresentPaymentEligibilityStore: Current default site missing." + + DDLogError(message) + + crashLogger.logMessage( + "OrderCardPresentPaymentEligibilityStore: Current default site missing.", + properties: [ + "requestedSiteID": siteID + ], + level: .error + ) + } +} + extension OrderCardPresentPaymentEligibilityStore { enum OrderIsEligibleForCardPresentPaymentError: Error { case orderNotFoundInStorage - case siteNotFoundInStorage + case failedToObtainSite case cardReaderPaymentOptionIsNotSupportedForCIABSites } } diff --git a/Modules/Tests/PointOfSaleTests/Mocks/MockFeatureFlagService.swift b/Modules/Tests/PointOfSaleTests/Mocks/MockFeatureFlagService.swift index f76817d0f81..e974037853d 100644 --- a/Modules/Tests/PointOfSaleTests/Mocks/MockFeatureFlagService.swift +++ b/Modules/Tests/PointOfSaleTests/Mocks/MockFeatureFlagService.swift @@ -25,6 +25,7 @@ final class MockFeatureFlagService: POSFeatureFlagProviding { var isProductImageOptimizedHandlingEnabled: Bool var isFeatureFlagEnabledReturnValue: [FeatureFlag: Bool] = [:] var isCIABBookingsEnabled: Bool + var isCIABEnabled: Bool init(isInboxOn: Bool = false, isShowInboxCTAEnabled: Bool = false, @@ -47,7 +48,8 @@ final class MockFeatureFlagService: POSFeatureFlagProviding { notificationSettings: Bool = false, allowMerchantAIAPIKey: Bool = false, isProductImageOptimizedHandlingEnabled: Bool = false, - isCIABBookingsEnabled: Bool = false) { + isCIABBookingsEnabled: Bool = false, + isCIABEnabled: Bool = false) { self.isInboxOn = isInboxOn self.isShowInboxCTAEnabled = isShowInboxCTAEnabled self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn @@ -70,6 +72,7 @@ final class MockFeatureFlagService: POSFeatureFlagProviding { self.allowMerchantAIAPIKey = allowMerchantAIAPIKey self.isProductImageOptimizedHandlingEnabled = isProductImageOptimizedHandlingEnabled self.isCIABBookingsEnabled = isCIABBookingsEnabled + self.isCIABEnabled = isCIABEnabled } func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool { @@ -124,6 +127,8 @@ final class MockFeatureFlagService: POSFeatureFlagProviding { return isProductImageOptimizedHandlingEnabled case .ciabBookings: return isCIABBookingsEnabled + case .ciab: + return isCIABEnabled default: return false } diff --git a/Modules/Tests/YosemiteTests/Stores/OrderCardPresentPaymentEligibilityStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/OrderCardPresentPaymentEligibilityStoreTests.swift index 444a60e7512..e4a17a5b55f 100644 --- a/Modules/Tests/YosemiteTests/Stores/OrderCardPresentPaymentEligibilityStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/OrderCardPresentPaymentEligibilityStoreTests.swift @@ -4,6 +4,7 @@ import XCTest @testable import Yosemite @testable import Networking +@testable import WooFoundation final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase { @@ -28,6 +29,7 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase { private var store: OrderCardPresentPaymentEligibilityStore! private var currentSite: Site? + private var isCIABSupported = true override func setUp() { super.setUp() @@ -38,6 +40,10 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase { dispatcher: dispatcher, storageManager: storageManager, network: network, + crashLogger: MockCrashLogger(), + isCIABEnvironmentSupported: { [weak self] in + return self?.isCIABSupported ?? false + }, currentSite: { [weak self] in return self?.currentSite } @@ -46,6 +52,7 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase { override func tearDown() { currentSite = nil + isCIABSupported = true super.tearDown() } @@ -98,6 +105,53 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase { XCTAssertTrue(eligibility) } + func test_orderIsEligibleForCardPresentPayment_returns_true_for_eligible_order_and_none_stored_site() throws { + // Given + let orderItem = OrderItem.fake().copy(itemID: 1234, + name: "Chocolate cake", + productID: 678, + quantity: 1.0) + let cppEligibleOrder = Order.fake().copy(siteID: sampleSiteID, + orderID: 111, + status: .pending, + currency: "USD", + datePaid: nil, + total: "5.00", + paymentMethodID: "woocommerce_payments", + items: [orderItem]) + let nonSubscriptionProduct = Product.fake().copy(siteID: sampleSiteID, + productID: 678, + name: "Chocolate cake", + productTypeKey: "simple") + + let regularSite = Site.fake().copy( + siteID: sampleSiteID, + isGarden: false, + gardenName: nil + ) + self.currentSite = regularSite + + storageManager.insertSampleProduct(readOnlyProduct: nonSubscriptionProduct) + storageManager.insertSampleOrder(readOnlyOrder: cppEligibleOrder) + + let configuration = CardPresentPaymentsConfiguration(country: .US) + + // When + let result = waitFor { promise in + let action = OrderCardPresentPaymentEligibilityAction + .orderIsEligibleForCardPresentPayment(orderID: 111, + siteID: self.sampleSiteID, + cardPresentPaymentsConfiguration: configuration) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let eligibility = try XCTUnwrap(result.get()) + XCTAssertTrue(eligibility) + } + func test_orderIsEligibleForCardPresentPayment_returns_failure_for_CIAB_sites() throws { // Given let orderItem = OrderItem.fake().copy(itemID: 1234, @@ -147,4 +201,56 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase { .cardReaderPaymentOptionIsNotSupportedForCIABSites) } } + + func test_orderIsEligibleForCardPresentPayment_returns_success_when_site_is_CIAB_and_CIAB_not_supported() throws { + // Given + + /// Simulate that the CIAB environment support is not yet rolled out + isCIABSupported = false + + let orderItem = OrderItem.fake().copy(itemID: 1234, + name: "Chocolate cake", + productID: 678, + quantity: 1.0) + let cppEligibleOrder = Order.fake().copy(siteID: sampleSiteID, + orderID: 111, + status: .pending, + currency: "USD", + datePaid: nil, + total: "5.00", + paymentMethodID: "woocommerce_payments", + items: [orderItem]) + let nonSubscriptionProduct = Product.fake().copy(siteID: sampleSiteID, + productID: 678, + name: "Chocolate cake", + productTypeKey: "simple") + + let ciabSite = Site.fake().copy( + siteID: sampleSiteID, + isGarden: true, + gardenName: "commerce" + ) + self.currentSite = ciabSite + + storageManager.insertSampleSite(readOnlySite: ciabSite) + storageManager.insertSampleProduct(readOnlyProduct: nonSubscriptionProduct) + storageManager.insertSampleOrder(readOnlyOrder: cppEligibleOrder) + + let configuration = CardPresentPaymentsConfiguration(country: .US) + + // When + let result = waitFor { promise in + let action = OrderCardPresentPaymentEligibilityAction + .orderIsEligibleForCardPresentPayment(orderID: 111, + siteID: self.sampleSiteID, + cardPresentPaymentsConfiguration: configuration) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let eligibility = try XCTUnwrap(result.get()) + XCTAssertTrue(eligibility) + } } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e365785f354..7481924c2ca 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -13,6 +13,7 @@ - [*] Fix order details presentation when opened from booking details [https://github.com/woocommerce/woocommerce-ios/pull/16331] - [*] Show POS feedback surveys for eligible merchants [https://github.com/woocommerce/woocommerce-ios/pull/16325] - [*] Fix product variation selection for order creation [https://github.com/woocommerce/woocommerce-ios/pull/16317] +- [*] Attempt to fix missing card payment options [https://github.com/woocommerce/woocommerce-ios/pull/16411] - [internal] Hide non-CIAB product types from filters [https://github.com/woocommerce/woocommerce-ios/pull/16354] - [Internal] Fix warning when displaying offline banner on My Store [https://github.com/woocommerce/woocommerce-ios/pull/16347] - [Internal] Notify listeners immediately after updating predicates or sort descriptors for results controllers. [https://github.com/woocommerce/woocommerce-ios/pull/16350] diff --git a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift index af745e264a5..20d32ab2236 100644 --- a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift @@ -86,8 +86,12 @@ class AuthenticatedState: StoresManagerState { dispatcher: dispatcher, storageManager: storageManager, network: network, + crashLogger: ServiceLocator.crashLogging, + isCIABEnvironmentSupported: { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.ciab) + }, currentSite: { - ServiceLocator.stores.sessionManager.defaultSite + sessionManager.defaultSite } ), OrderNoteStore(dispatcher: dispatcher, storageManager: storageManager, network: network), diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift index a2430966297..ef567c429b5 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift @@ -2,11 +2,11 @@ import Foundation import XCTest import Combine import Fakes -import WooFoundation @testable import WooCommerce @testable import Yosemite @testable import Networking +@testable import WooFoundation private typealias Dependencies = PaymentMethodsViewModel.Dependencies @@ -807,6 +807,8 @@ final class PaymentMethodsViewModelTests: XCTestCase { dispatcher: Dispatcher(), storageManager: storage, network: MockNetwork(), + crashLogger: MockCrashLogger(), + isCIABEnvironmentSupported: { true }, currentSite: { ciabSite } )