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
1 change: 1 addition & 0 deletions Modules/Sources/Yosemite/Base/StoresManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public protocol StoresManager {

/// The currently logged in store/site ID. Nil when the app is logged out.
///
/// periphery: ignore - used in tests
var siteID: AnyPublisher<Int64?, Never> { get }

/// Observable currently selected site.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,7 @@ struct ScreenshotObjectGraph: MockObjectGraph {
gravatarUrl: nil
)

let defaultSite = Site(
siteID: 1,
name: Defaults.Site.name,
description: "",
url: Defaults.Site.url,
adminURL: Defaults.Site.adminURL,
loginURL: Defaults.Site.loginURL,
isSiteOwner: false,
frameNonce: "",
plan: "",
isAIAssistantFeatureActive: false,
isJetpackThePluginInstalled: true,
isJetpackConnected: true,
isWooCommerceActive: true,
isWordPressComStore: false,
jetpackConnectionActivePlugins: [],
timezone: "UTC",
gmtOffset: 0,
visibility: .publicSite,
canBlaze: false,
isAdmin: false,
wasEcommerceTrial: false,
hasSSOEnabled: false,
applicationPasswordAvailable: false,
isGarden: false,
gardenName: nil,
gardenPartner: nil
)
let defaultSite = Site.defaultMock()

/// May not be needed anymore if we're not mocking the API
let defaultSiteAPI = SiteAPI(siteID: 1, namespaces: [
Expand Down
35 changes: 35 additions & 0 deletions Modules/Sources/Yosemite/Model/Mocks/Site+Mocks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import struct Networking.Site

public extension Site {
static func defaultMock() -> Self {
return Site(
siteID: 1,
name: Defaults.Site.name,
description: "",
url: Defaults.Site.url,
adminURL: Defaults.Site.adminURL,
loginURL: Defaults.Site.loginURL,
isSiteOwner: false,
frameNonce: "",
plan: "",
isAIAssistantFeatureActive: false,
isJetpackThePluginInstalled: true,
isJetpackConnected: true,
isWooCommerceActive: true,
isWordPressComStore: false,
jetpackConnectionActivePlugins: [],
timezone: "UTC",
gmtOffset: 0,
visibility: .publicSite,
canBlaze: false,
isAdmin: false,
wasEcommerceTrial: false,
hasSSOEnabled: false,
applicationPasswordAvailable: false,
isGarden: false,
gardenName: nil,
gardenPartner: nil
)
}
}
1 change: 1 addition & 0 deletions WooCommerce/Classes/CIAB/CIABAffectedFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum CIABAffectedFeature: CaseIterable {
case variableProducts
case giftCardEditing
case productsStockDashboardCard
case pointOfSale
}

extension CIABAffectedFeature {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ private extension POSIneligibleReason {
return "unknown_wc_plugin"
case .unsupportedIOSVersion:
return "ios_version"
case .unsupportedInCIABSites:
return "feature_unsupported_in_ciab"
case .siteSettingsNotAvailable,
.selfDeallocated:
return "other"
Expand Down
1 change: 1 addition & 0 deletions WooCommerce/Classes/POS/Models/POSIneligibleReason.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum POSIneligibleReason: Equatable {
case featureSwitchDisabled
case unsupportedCurrency(countryCode: CountryCode, supportedCurrencies: [CurrencyCode])
case selfDeallocated
case unsupportedInCIABSites
}

/// Represents the eligibility state for POS.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ struct PointOfSaleEntryPointView: View {
searchHistoryService: PointOfSalePreviewHistoryService(),
popularPurchasableItemsController: PointOfSalePreviewItemsController(),
barcodeScanService: PointOfSalePreviewBarcodeScanService(),
posEligibilityChecker: POSTabEligibilityChecker(siteID: 0),
posEligibilityChecker: POSTabEligibilityChecker(site: .defaultMock()),
services: POSPreviewServices())
}

Expand Down
12 changes: 11 additions & 1 deletion WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ struct POSIneligibleView: View {
return NSLocalizedString("pos.ineligible.suggestion.selfDeallocated",
value: "Try relaunching the app to resolve this issue.",
comment: "Suggestion for self deallocated: relaunch")
case .unsupportedInCIABSites:
return NSLocalizedString(
"pos.ineligible.suggestion.notSupportedForCIAB",
value: "The POS system is not supported for your store.",
comment: "Suggestion for CIAB sites: feature is not supported"
)
}
}
}
Expand All @@ -177,7 +183,8 @@ private extension POSIneligibleView {
private extension POSIneligibleReason {
var shouldShowRetryButton: Bool {
switch self {
case .unsupportedIOSVersion:
case .unsupportedIOSVersion,
.unsupportedInCIABSites:
return false
case .unsupportedWooCommerceVersion,
.siteSettingsNotAvailable,
Expand Down Expand Up @@ -208,6 +215,9 @@ private extension POSIneligibleReason {
value: "Retry",
comment: "Button title to refresh POS eligibility check"
)
case .unsupportedInCIABSites:
assertionFailure("Retry button should not be shown for `unsupportedInCIABSites`")
return String()
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions WooCommerce/Classes/POS/Utils/PreviewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import struct Yosemite.POSOrderRefund
import typealias Yosemite.OrderItemAttribute
import class Yosemite.POSOrderListService
import class Yosemite.POSOrderListFetchStrategyFactory
import struct Yosemite.Site

// MARK: - PreviewProvider helpers
//
Expand Down Expand Up @@ -224,8 +225,9 @@ struct POSPreviewHelpers {
featureFlags: POSFeatureFlagProviding = EmptyPOSFeatureFlags()
) -> PointOfSaleAggregateModel {
return PointOfSaleAggregateModel(
entryPointController: POSEntryPointController(eligibilityChecker: LegacyPOSTabEligibilityChecker(siteID: 0),
featureFlagService: featureFlags),
entryPointController: POSEntryPointController(
eligibilityChecker: LegacyPOSTabEligibilityChecker(site: Site.defaultMock()),
featureFlagService: featureFlags),
itemsController: itemsController,
purchasableItemsSearchController: purchasableItemsSearchController,
couponsController: couponsController,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import enum WooFoundation.CountryCode
import enum WooFoundation.CurrencyCode
import protocol Experiments.FeatureFlagService
import struct Yosemite.SiteSetting
import struct Yosemite.Site
import protocol Yosemite.POSEligibilityServiceProtocol
import protocol Yosemite.StoresManager
import class Yosemite.POSEligibilityService
Expand All @@ -28,6 +29,7 @@ private enum LegacyPOSIneligibleReason: Equatable {
case unsupportedCountry(supportedCountries: [CountryCode])
case unsupportedCurrency(supportedCurrencies: [CurrencyCode])
case selfDeallocated
case unsupportedInCIABSites
}

/// Legacy POS eligibility state for i1.
Expand All @@ -38,33 +40,36 @@ private enum LegacyPOSEligibilityState: Equatable {

/// POS tab eligibility checker for i1. Will be replaced by `POSTabEligibilityCheckerI2` when removing `pointOfSaleAsATabi2` feature flag.
final class LegacyPOSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
private let siteID: Int64
private let site: Site
private let userInterfaceIdiom: UIUserInterfaceIdiom
private let siteSettings: SelectedSiteSettingsProtocol
private let pluginsService: PluginsServiceProtocol
private let eligibilityService: POSEligibilityServiceProtocol
private let stores: StoresManager
private let featureFlagService: FeatureFlagService
private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol

init(siteID: Int64,
init(site: Site,
userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom,
siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings,
pluginsService: PluginsServiceProtocol = PluginsService(storageManager: ServiceLocator.storageManager),
eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(),
stores: StoresManager = ServiceLocator.stores,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
self.siteID = siteID
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker()) {
self.site = site
self.userInterfaceIdiom = userInterfaceIdiom
self.siteSettings = siteSettings
self.pluginsService = pluginsService
self.eligibilityService = eligibilityService
self.stores = stores
self.featureFlagService = featureFlagService
self.siteCIABEligibilityChecker = siteCIABEligibilityChecker
}

/// Checks the initial visibility of the POS tab without dependance on network requests.
func checkInitialVisibility() -> Bool {
eligibilityService.loadCachedPOSTabVisibility(siteID: siteID) ?? false
eligibilityService.loadCachedPOSTabVisibility(siteID: site.siteID) ?? false
}

/// Determines whether the POS entry point can be shown based on the selected store and feature gates.
Expand All @@ -73,6 +78,10 @@ final class LegacyPOSTabEligibilityChecker: POSEntryPointEligibilityCheckerProto
}

private func checkI1Eligibility() async -> LegacyPOSEligibilityState {
guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else {
return .ineligible(reason: .unsupportedInCIABSites)
}

switch checkDeviceEligibility() {
case .eligible:
break
Expand Down Expand Up @@ -139,7 +148,7 @@ private extension LegacyPOSTabEligibilityChecker {

private extension LegacyPOSTabEligibilityChecker {
func checkPluginEligibility() async -> LegacyPOSEligibilityState {
let wcPlugin = await fetchWooCommercePlugin(siteID: siteID)
let wcPlugin = await fetchWooCommercePlugin(siteID: site.siteID)

guard VersionHelpers.isVersionSupported(version: wcPlugin.version,
minimumRequired: Constants.wcPluginMinimumVersion) else {
Expand All @@ -155,7 +164,7 @@ private extension LegacyPOSTabEligibilityChecker {
}

// For versions that support the feature switch, checks if the feature switch is enabled.
return await checkFeatureSwitchEnabled(siteID: siteID)
return await checkFeatureSwitchEnabled(siteID: site.siteID)
}

@MainActor
Expand Down Expand Up @@ -201,7 +210,7 @@ private extension LegacyPOSTabEligibilityChecker {

func waitForSiteSettingsRefresh() async -> [SiteSetting] {
for await siteSettings in siteSettings.settingsStream.values {
guard siteSettings.siteID == siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else {
guard siteSettings.siteID == site.siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else {
continue
}
return siteSettings.settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import enum WooFoundation.CountryCode
import enum WooFoundation.CurrencyCode
import protocol Experiments.FeatureFlagService
import struct Yosemite.SiteSetting
import struct Yosemite.Site
import protocol Yosemite.POSEligibilityServiceProtocol
import protocol Yosemite.StoresManager
import class Yosemite.POSEligibilityService
Expand All @@ -30,25 +31,27 @@ protocol POSEntryPointEligibilityCheckerProtocol {
}

final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
private let siteID: Int64
private let site: Site
private let userInterfaceIdiom: UIUserInterfaceIdiom
private let siteSettings: SelectedSiteSettingsProtocol
private let eligibilityService: POSEligibilityServiceProtocol
private let stores: StoresManager
private let featureFlagService: FeatureFlagService
private let systemStatusService: POSSystemStatusServiceProtocol
private let siteSettingService: POSSiteSettingServiceProtocol
private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol
private let appPasswordSupportState: ApplicationPasswordsExperimentState

init(siteID: Int64,
init(site: Site,
userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom,
siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings,
eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(),
stores: StoresManager = ServiceLocator.stores,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
systemStatusService: POSSystemStatusServiceProtocol? = nil,
siteSettingService: POSSiteSettingServiceProtocol? = nil) {
self.siteID = siteID
siteSettingService: POSSiteSettingServiceProtocol? = nil,
siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker()) {
self.site = site
self.userInterfaceIdiom = userInterfaceIdiom
self.siteSettings = siteSettings
self.eligibilityService = eligibilityService
Expand All @@ -58,27 +61,32 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {

let credentials = stores.sessionManager.defaultCredentials
let selectedSite = stores.sessionManager.defaultSitePublisher.map { $0?.toJetpackSite() }.eraseToAnyPublisher()
let appPasswordSupportState = appPasswordSupportState.$isAvailableAndEnabled.eraseToAnyPublisher()
let appPasswordSupport = appPasswordSupportState.$isAvailableAndEnabled.eraseToAnyPublisher()
self.systemStatusService = systemStatusService ?? POSSystemStatusService(
credentials: credentials,
selectedSite: selectedSite,
appPasswordSupportState: appPasswordSupportState,
appPasswordSupportState: appPasswordSupport,
storageManager: ServiceLocator.storageManager
)
self.siteSettingService = siteSettingService ?? POSSiteSettingService(
credentials: credentials,
selectedSite: selectedSite,
appPasswordSupportState: appPasswordSupportState
appPasswordSupportState: appPasswordSupport
)
self.siteCIABEligibilityChecker = siteCIABEligibilityChecker
}

/// Checks the initial visibility of the POS tab without dependance on network requests.
func checkInitialVisibility() -> Bool {
eligibilityService.loadCachedPOSTabVisibility(siteID: siteID) ?? false
eligibilityService.loadCachedPOSTabVisibility(siteID: site.siteID) ?? false
}

/// Determines whether the POS entry point can be shown based on the selected store and feature gates.
func checkEligibility() async -> POSEligibilityState {
guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else {
return .ineligible(reason: .unsupportedInCIABSites)
}

guard #available(iOS 17.0, *) else {
return .ineligible(reason: .unsupportedIOSVersion)
}
Expand All @@ -103,6 +111,10 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {

/// Checks the final visibility of the POS tab.
func checkVisibility() async -> Bool {
guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else {
return false
}

guard userInterfaceIdiom == .pad else {
return false
}
Expand Down Expand Up @@ -137,10 +149,12 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
case .unsupportedWooCommerceVersion, .wooCommercePluginNotFound:
return await checkEligibility()
case .featureSwitchDisabled:
_ = try await siteSettingService.setFeature(siteID: siteID, feature: .pointOfSale, enabled: true)
_ = try await siteSettingService.setFeature(siteID: site.siteID, feature: .pointOfSale, enabled: true)
return await checkEligibility()
case .selfDeallocated:
return await checkEligibility()
case .unsupportedInCIABSites:
return await checkEligibility()
}
}
}
Expand All @@ -154,7 +168,7 @@ private extension POSTabEligibilityChecker {
/// - Returns: The eligibility state for POS based on the WooCommerce plugin and POS feature switch.
func checkPluginEligibility() async -> POSEligibilityState {
do {
let info = try await systemStatusService.loadWooCommercePluginAndPOSFeatureSwitch(siteID: siteID)
let info = try await systemStatusService.loadWooCommercePluginAndPOSFeatureSwitch(siteID: site.siteID)
let wcPluginEligibility = checkWooCommercePluginEligibility(wcPlugin: info.wcPlugin)
switch wcPluginEligibility {
case .eligible:
Expand Down Expand Up @@ -247,7 +261,7 @@ private extension POSTabEligibilityChecker {

func waitForSiteSettingsRefresh() async -> [SiteSetting] {
for await siteSettings in siteSettings.settingsStream.values {
guard siteSettings.siteID == siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else {
guard siteSettings.siteID == site.siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else {
continue
}
return siteSettings.settings
Expand Down Expand Up @@ -279,7 +293,7 @@ private extension POSTabEligibilityChecker {
guard let self else {
return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated)
}
stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: siteID) { [weak self] error in
stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: site.siteID) { [weak self] error in
guard let self else {
return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated)
}
Expand Down
Loading