Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
default:
return true
}
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 @@ -195,4 +195,8 @@ public enum FeatureFlag: Int {
/// Enables displaying POS as a tab in the tab bar with the same eligibility as the previous entry point
///
case pointOfSaleAsATabi1

/// Enables displaying POS as a tab in the tab bar for stores in eligible countries
///
case pointOfSaleAsATabi2
}
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