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
2 changes: 2 additions & 0 deletions Modules/Sources/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return false
case .pointOfSaleAsATabi1:
return true
case .pointOfSaleAsATabi2:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .pointOfSaleOrdersi1:
return buildConfig == .localDeveloper || buildConfig == .alpha
default:
Expand Down
4 changes: 4 additions & 0 deletions Modules/Sources/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ public enum FeatureFlag: Int {
///
case pointOfSaleAsATabi1

/// Enables displaying POS as a tab in the tab bar for stores in eligible countries
///
case pointOfSaleAsATabi2

/// Enables displaying Point Of Sale details in order list and order details
///
case pointOfSaleOrdersi1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ enum POSEligibilityState: Equatable {
protocol POSEntryPointEligibilityCheckerProtocol {
/// Checks the initial visibility of the POS tab.
func checkInitialVisibility() -> Bool
/// Checks the final visibility of the POS tab.
func checkVisibility() async -> Bool
/// Determines whether the site is eligible for POS.
func checkEligibility() async -> POSEligibilityState
}
Expand Down Expand Up @@ -74,12 +76,11 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {

/// Determines whether the POS entry point can be shown based on the selected store and feature gates.
func checkEligibility() async -> POSEligibilityState {
guard #available(iOS 17.0, *) else {
return .ineligible(reason: .unsupportedIOSVersion)
}

guard userInterfaceIdiom == .pad else {
return .ineligible(reason: .notTablet)
switch checkDeviceEligibility() {
case .eligible:
break
case .ineligible(let reason):
return .ineligible(reason: reason)
}

async let siteSettingsEligibility = checkSiteSettingsEligibility()
Expand Down Expand Up @@ -110,8 +111,61 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
return .ineligible(reason: reason)
}
}

/// Checks the final visibility of the POS tab.
func checkVisibility() async -> Bool {
if featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi2) {
return await checkVisibilityBasedOnCountryAndRemoteFeatureFlag()
} else {
let eligibility = await checkEligibility()
return eligibility == .eligible
}
}
}

private extension POSTabEligibilityChecker {
func checkDeviceEligibility() -> POSEligibilityState {
guard #available(iOS 17.0, *) else {
return .ineligible(reason: .unsupportedIOSVersion)
}

guard userInterfaceIdiom == .pad else {
return .ineligible(reason: .notTablet)
}

return .eligible
}

func checkVisibilityBasedOnCountryAndRemoteFeatureFlag() async -> Bool {
guard checkDeviceEligibility() == .eligible else {
return false
}

async let siteSettingsEligibility = checkSiteSettingsEligibility()
async let featureFlagEligibility = checkRemoteFeatureEligibility()

switch await siteSettingsEligibility {
case .eligible:
break
case let .ineligible(reason):
if reason == .unsupportedCurrency {
break
Copy link
Contributor

Choose a reason for hiding this comment

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

As I understand, it's intentional to show POS option with unsupported currency for i2?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, as in the delivery plan pdfdoF-7po-p2, we're enabling the POS tab for all stores in eligible countries (US and UK for now).

} else {
return false
}
}

switch await featureFlagEligibility {
case .eligible:
return true
case .ineligible:
return false
}
}
}

// MARK: - WC Plugin Related Eligibility Check

private extension POSTabEligibilityChecker {
func checkPluginEligibility() async -> POSEligibilityState {
let wcPlugin = await fetchWooCommercePlugin(siteID: siteID)
Expand Down Expand Up @@ -157,6 +211,8 @@ private extension POSTabEligibilityChecker {
}
}

// MARK: - Site Settings Related Eligibility Check

private extension POSTabEligibilityChecker {
func checkSiteSettingsEligibility() async -> POSEligibilityState {
// Waits for the first site settings that matches the given site ID.
Expand Down Expand Up @@ -202,6 +258,8 @@ private extension POSTabEligibilityChecker {
}
}

// MARK: - Remote Feature Flag Eligibility Check

private extension POSTabEligibilityChecker {
@MainActor
func checkRemoteFeatureEligibility() async -> POSEligibilityState {
Expand Down
3 changes: 1 addition & 2 deletions WooCommerce/Classes/ViewRelated/MainTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -670,8 +670,7 @@ private extension MainTabBarController {
// Starts observing the POS eligibility state.
posEligibilityCheckTask = Task { @MainActor [weak self] in
guard let self, let posEligibilityChecker = self.posEligibilityChecker else { return }
let eligibility = await posEligibilityChecker.checkEligibility()
let isPOSTabVisible = eligibility == .eligible
let isPOSTabVisible = await posEligibilityChecker.checkVisibility()
analytics.track(.pointOfSaleTabVisibilityChecked, withProperties: ["is_visible": isPOSTabVisible])
cachePOSTabVisibility(siteID: siteID, isPOSTabVisible: isPOSTabVisible)
updateTabViewControllers(isPOSTabVisible: isPOSTabVisible)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class MockFeatureFlagService: FeatureFlagService {
var isSubscriptionsInOrderCreationCustomersEnabled: Bool
var isSubscriptionsInOrderCreationUIEnabled: Bool
var isPointOfSaleEnabled: Bool
var isPointOfSaleAsATabi2Enabled: Bool
var googleAdsCampaignCreationOnWebView: Bool
var blazeEvergreenCampaigns: Bool
var blazeCampaignObjective: Bool
Expand All @@ -37,6 +38,7 @@ final class MockFeatureFlagService: FeatureFlagService {
isSubscriptionsInOrderCreationCustomersEnabled: Bool = false,
isSubscriptionsInOrderCreationUIEnabled: Bool = false,
isPointOfSaleEnabled: Bool = false,
isPointOfSaleAsATabi2Enabled: Bool = false,
googleAdsCampaignCreationOnWebView: Bool = false,
blazeEvergreenCampaigns: Bool = false,
blazeCampaignObjective: Bool = false,
Expand All @@ -58,6 +60,7 @@ final class MockFeatureFlagService: FeatureFlagService {
self.isSubscriptionsInOrderCreationCustomersEnabled = isSubscriptionsInOrderCreationCustomersEnabled
self.isSubscriptionsInOrderCreationUIEnabled = isSubscriptionsInOrderCreationUIEnabled
self.isPointOfSaleEnabled = isPointOfSaleEnabled
self.isPointOfSaleAsATabi2Enabled = isPointOfSaleAsATabi2Enabled
self.googleAdsCampaignCreationOnWebView = googleAdsCampaignCreationOnWebView
self.blazeEvergreenCampaigns = blazeEvergreenCampaigns
self.blazeCampaignObjective = blazeCampaignObjective
Expand Down Expand Up @@ -101,6 +104,8 @@ final class MockFeatureFlagService: FeatureFlagService {
return isSubscriptionsInOrderCreationUIEnabled
case .pointOfSale:
return isPointOfSaleEnabled
case .pointOfSaleAsATabi2:
return isPointOfSaleAsATabi2Enabled
case .googleAdsCampaignCreationOnWebView:
return googleAdsCampaignCreationOnWebView
case .blazeEvergreenCampaigns:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import Foundation

final class MockPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
var initialVisibility: Bool = false
var result: POSEligibilityState = .eligible
var visibility: Bool = false
var eligibility: POSEligibilityState = .eligible

func checkInitialVisibility() -> Bool {
initialVisibility
}

@MainActor
func checkVisibility() async -> Bool {
visibility
}

@MainActor
func checkEligibility() async -> POSEligibilityState {
result
eligibility
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ final class MainTabBarController_TabsTests: XCTestCase {
isAnInstanceOf: HubMenuViewController.self)
}

func test_tab_view_controllers_include_pos_tab_when_pos_is_eligible() throws {
func test_tab_view_controllers_include_pos_tab_when_pos_tab_is_visible() throws {
// Given
let featureFlagService = MockFeatureFlagService()
featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true

let mockPOSEligibilityChecker = MockPOSEligibilityChecker()
mockPOSEligibilityChecker.result = .eligible
mockPOSEligibilityChecker.visibility = true

let storesManager = MockStoresManager(sessionManager: .makeForTesting())

Expand Down Expand Up @@ -85,13 +85,13 @@ final class MainTabBarController_TabsTests: XCTestCase {
isAnInstanceOf: HubMenuViewController.self)
}

func test_tab_view_controllers_exclude_pos_tab_when_pos_is_not_eligible() throws {
func test_tab_view_controllers_exclude_pos_tab_when_pos_tab_is_not_visible() throws {
// Given
let featureFlagService = MockFeatureFlagService()
featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true

let mockPOSEligibilityChecker = MockPOSEligibilityChecker()
mockPOSEligibilityChecker.result = .ineligible(reason: .notTablet)
mockPOSEligibilityChecker.visibility = false

let storesManager = MockStoresManager(sessionManager: .makeForTesting())

Expand Down Expand Up @@ -132,7 +132,7 @@ final class MainTabBarController_TabsTests: XCTestCase {
featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = false

let mockPOSEligibilityChecker = MockPOSEligibilityChecker()
mockPOSEligibilityChecker.result = .eligible
mockPOSEligibilityChecker.visibility = true

let storesManager = MockStoresManager(sessionManager: .makeForTesting())

Expand Down Expand Up @@ -165,13 +165,13 @@ final class MainTabBarController_TabsTests: XCTestCase {
isAnInstanceOf: HubMenuViewController.self)
}

func test_tab_view_controllers_do_not_change_when_pos_eligibility_changes() throws {
func test_tab_view_controllers_do_not_change_when_pos_visibility_changes() throws {
// Given
let featureFlagService = MockFeatureFlagService()
featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true

let mockPOSEligibilityChecker = MockPOSEligibilityChecker()
mockPOSEligibilityChecker.result = .ineligible(reason: .featureFlagDisabled)
mockPOSEligibilityChecker.visibility = false

let storesManager = MockStoresManager(sessionManager: .makeForTesting())

Expand All @@ -196,7 +196,7 @@ final class MainTabBarController_TabsTests: XCTestCase {
}

// When - change POS eligibility
mockPOSEligibilityChecker.result = .eligible
mockPOSEligibilityChecker.visibility = true

// Then tabs remain the same
XCTAssertEqual(tabBarController.tabRootViewControllers.count, 4)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ final class MainTabBarControllerTests: XCTestCase {

// Hides POS tab.
let mockPOSEligibilityChecker = MockPOSEligibilityChecker()
mockPOSEligibilityChecker.result = .ineligible(reason: .featureFlagDisabled)
mockPOSEligibilityChecker.visibility = false

let storesManager = MockStoresManager(sessionManager: .testingInstance)
// Reset `receivedActions`
Expand Down Expand Up @@ -504,8 +504,8 @@ final class MainTabBarControllerTests: XCTestCase {
posTabContainerController.presentedViewController is UIHostingController<PointOfSaleEntryPointView>
}

// When returning POS eligibility as ineligible
mockPOSEligibilityChecker.setEligibilityResult(.ineligible(reason: .featureFlagDisabled))
// When POS tab becomes invisible
mockPOSEligibilityChecker.setVisibilityResult(false)

// Then POS tab is hidden
waitUntil {
Expand Down Expand Up @@ -551,7 +551,7 @@ final class MainTabBarControllerTests: XCTestCase {

// When POS tab initial visibility is set to true
stores.updateDefaultStore(storeID: 1216)
mockPOSEligibilityChecker.setEligibilityResult(.eligible)
mockPOSEligibilityChecker.setVisibilityResult(true)

waitUntil {
tabBarController.tabRootViewControllers.count == 5
Expand All @@ -567,7 +567,7 @@ final class MainTabBarControllerTests: XCTestCase {
featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true

let mockPOSEligibilityChecker = MockPOSEligibilityChecker()
mockPOSEligibilityChecker.result = .eligible
mockPOSEligibilityChecker.visibility = true

let storesManager = MockStoresManager(sessionManager: .makeForTesting())

Expand Down Expand Up @@ -642,9 +642,19 @@ extension MainTabBarController {

private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
var initialVisibility: Bool = false
private var visibilityResult: Bool?
private var visibilityContinuation: CheckedContinuation<Bool, Never>?
private var eligibilityResult: POSEligibilityState?
private var eligibilityContinuation: CheckedContinuation<POSEligibilityState, Never>?

func setVisibilityResult(_ result: Bool) {
visibilityResult = result
if let continuation = visibilityContinuation {
visibilityContinuation = nil
continuation.resume(returning: result)
}
}

func setEligibilityResult(_ result: POSEligibilityState) {
eligibilityResult = result
if let continuation = eligibilityContinuation {
Expand All @@ -657,6 +667,19 @@ private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityChec
initialVisibility
}

func checkVisibility() async -> Bool {
if let visibilityResult {
return visibilityResult
}
return await withCheckedContinuation { continuation in
visibilityContinuation = continuation
// If we already have a result, return it immediately.
if visibilityContinuation == nil {
continuation.resume(returning: visibilityResult ?? true)
}
}
}

func checkEligibility() async -> POSEligibilityState {
if let eligibilityResult {
return eligibilityResult
Expand Down
Loading