Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
284 changes: 207 additions & 77 deletions WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,129 +2,259 @@ import SwiftUI

/// A view that displays when the Point of Sale (POS) feature is not available for the current store.
/// Shows the specific reason why POS is ineligible and provides a button to re-check eligibility.
@available(iOS 17.0, *)
struct POSIneligibleView: View {
let reason: POSIneligibleReason
let onRefresh: () async throws -> Void
@Environment(\.dismiss) private var dismiss
@State private var isLoading: Bool = false

var body: some View {
VStack(spacing: POSSpacing.large) {
HStack {
Spacer()
Button {
dismiss()
} label: {
Text(Image(systemName: "xmark"))
.font(POSFontStyle.posButtonSymbolLarge.font())
}
.foregroundColor(Color.posOnSurfaceVariantLowest)
}

VStack(spacing: 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we have POSSpacing.none if you want. It doesn't make a big difference though – I can't see us ever changing the value of that constant!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, updated in 33eb215.

Spacer()

VStack(spacing: POSSpacing.medium) {
VStack(alignment: .center, spacing: 0) {
Image(PointOfSaleAssets.exclamationMark.imageName)
.resizable()
.frame(width: POSErrorAndAlertIconSize.large.dimension,
height: POSErrorAndAlertIconSize.large.dimension)

Text(reasonText)
.font(POSFontStyle.posHeadingBold.font())
.multilineTextAlignment(.center)
.foregroundColor(Color.posOnSurface)

Button {
Task { @MainActor in
do {
isLoading = true
try await onRefresh()
isLoading = false
} catch {
// TODO-jc: handle error if needed, e.g., show an error message
print("Error refreshing eligibility: \(error)")
isLoading = false
Spacer()
.frame(height: POSSpacing.medium)

VStack(spacing: POSSpacing.small) {
Text(Localization.title)
.font(POSFontStyle.posHeadingBold.font())
.multilineTextAlignment(.center)
.foregroundColor(Color.posOnSurface)

Text(suggestionText)
.font(POSFontStyle.posBodyLargeRegular().font())
.multilineTextAlignment(.center)
.foregroundColor(Color.posOnSurface)
}
.containerRelativeFrame(.horizontal) { length, _ in
length * 0.5
}

Spacer()
.frame(height: POSSpacing.large)

VStack(spacing: POSSpacing.medium) {
Button {
Task { @MainActor in
do {
isLoading = true
try await onRefresh()
isLoading = false
} catch {
// TODO: WOOMOB-720 - handle error if needed, e.g., show an error message
print("Error refreshing eligibility: \(error)")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: DDLogError

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, updated in f95efc5.

isLoading = false
}
}
} label: {
Text(Localization.refreshEligibility)
}
} label: {
Text(Localization.refreshEligibility)
.buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: isLoading))

Button {
dismiss()
} label: {
Text(Localization.dismiss)
}
.buttonStyle(POSOutlinedButtonStyle(size: .normal))
}
.containerRelativeFrame(.horizontal) { length, _ in
length * 0.5 - 132
Copy link
Contributor

Choose a reason for hiding this comment

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

This size prevents any text from showing on the buttons when in a narrow split view, and won't work when we add POS to the phone. I think this needs a minimum value...
narrow buttons in a split view with no text

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the width for the text and buttons to be at least 300px in 156eefe, given that the current iOS devices having 320px minimum width. There's still room for improvement for larger font sizes as you mentioned in WOOMOB-750.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Largest font size:

split view 100% width
Simulator Screenshot - iPad Air 13-inch (M3) - 2025-07-03 at 09 46 58 Simulator Screenshot - iPad Air 13-inch (M3) - 2025-07-03 at 09 51 32

Smaller font size:

split view 100% width
Simulator Screenshot - iPad Air 13-inch (M3) - 2025-07-03 at 09 52 41 Simulator Screenshot - iPad Air 13-inch (M3) - 2025-07-03 at 09 52 27

}
.buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: isLoading))
}

Spacer()
}
.padding(POSPadding.large)
}

private var reasonText: String {
private var suggestionText: String {
switch reason {
case .notTablet:
return NSLocalizedString("pos.ineligible.reason.notTablet",
value: "POS is only available on iPad.",
comment: "Ineligible reason: not a tablet")
return NSLocalizedString("pos.ineligible.suggestion.notTablet",
value: "Please use a tablet to access POS features.",
comment: "Suggestion for not tablet: use iPad")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the tab show up on the phone? I can't see it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not now and in the near future, but it's an eligibility check that I thought it's better to throw an ineligible error than fatalError or no-op. I can separate the ineligible reason enum into two, one for visibility and the other for eligibility so that this case won't have to be handled. Created an issue in WOOMOB-756.

case .unsupportedIOSVersion:
return NSLocalizedString("pos.ineligible.reason.unsupportedIOSVersion",
value: "POS requires a newer version of iOS 17 and above.",
comment: "Ineligible reason: iOS version too low")
case .unsupportedWooCommerceVersion:
return NSLocalizedString("pos.ineligible.reason.unsupportedWooCommerceVersion",
value: "Please update WooCommerce plugin to use POS.",
comment: "Ineligible reason: WooCommerce version too low")
return NSLocalizedString("pos.ineligible.suggestion.unsupportedIOSVersion",
value: "The POS system requires iOS 17 or later. Please update your device to iOS 17+ to use this feature.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
value: "The POS system requires iOS 17 or later. Please update your device to iOS 17+ to use this feature.",
value: "Point of Sale requires iOS 17 or later. Please update your device to iOS 17+ to use this feature.",

"The POS system" feels quite clumsy to read, I'd suggest replacing it with "Point of Sale" everywhere as it's clearer and shorter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy updated in 8ae5ba5.

comment: "Suggestion for unsupported iOS version: update iOS")
case let .unsupportedWooCommerceVersion(minimumVersion):
let format = NSLocalizedString("pos.ineligible.suggestion.unsupportedWooCommerceVersion",
value: "Your WooCommerce version is not supported. " +
"The POS system requires WooCommerce version %1$@ or above. Please update WooCommerce to the latest version.",
comment: "Suggestion for unsupported WooCommerce version: update plugin. " +
"%1$@ is a placeholder for the minimum required version.")
return String.localizedStringWithFormat(format, minimumVersion)
case .wooCommercePluginNotFound:
return NSLocalizedString("pos.ineligible.reason.wooCommercePluginNotFound",
value: "WooCommerce plugin not found.",
comment: "Ineligible reason: plugin missing")
return NSLocalizedString("pos.ineligible.suggestion.wooCommercePluginNotFound",
value: "Install and activate the WooCommerce plugin from your WordPress admin.",
comment: "Suggestion for missing WooCommerce plugin: install plugin")
case .featureSwitchDisabled:
return NSLocalizedString("pos.ineligible.reason.featureSwitchDisabled",
value: "POS feature is not enabled for your store.",
comment: "Ineligible reason: feature switch off")
return NSLocalizedString("pos.ineligible.suggestion.featureSwitchDisabled",
value: "The POS core feature must be enabled to proceed. " +
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
value: "The POS core feature must be enabled to proceed. " +
value: "Point of Sale must be enabled to proceed. " +

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy updated in 8ae5ba5.

"Please enable the POS feature from your WordPress admin under WooCommerce settings > Advanced > Features.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an API we can use to just do this when people tap a button on this screen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, there is pdfdoF-6VP-p2#endpoint, I created an issue to integrate with the API in WOOMOB-759.

comment: "Suggestion for disabled feature switch: enable feature in WooCommerce settings")
case .featureSwitchSyncFailure:
return NSLocalizedString("pos.ineligible.reason.featureSwitchSyncFailure",
value: "Could not verify POS feature status.",
comment: "Ineligible reason: feature switch sync failed")
case .unsupportedCountry:
return NSLocalizedString("pos.ineligible.reason.unsupportedCountry",
value: "POS is not available in your country.",
comment: "Ineligible reason: country not supported")
case .unsupportedCurrency:
return NSLocalizedString("pos.ineligible.reason.unsupportedCurrency",
value: "POS is not available for your store's currency.",
comment: "Ineligible reason: currency not supported")
return NSLocalizedString("pos.ineligible.suggestion.featureSwitchSyncFailure",
value: "Try relaunching the app or check your internet connection and try again.",
comment: "Suggestion for feature switch sync failure: relaunch or check connection")
case let .unsupportedCountry(supportedCountries):
let countryNames = supportedCountries.map { $0.readableCountry }
let formattedCountryList = ListFormatter.localizedString(byJoining: countryNames)
let format = NSLocalizedString(
"pos.ineligible.suggestion.unsupportedCountry",
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the tab in unsupported countries yet; I'm assuming that's just not changed yet 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar to the previous comment https://github.com/woocommerce/woocommerce-ios/pull/15859/files#r2182744134, it's because the ineligible enum includes the cases that make the POS tab invisible. I will handle that in the separate issue linked there.

value: "POS is currently only available in %1$@. Check back later for availability in your region.",
comment: "Suggestion for unsupported country with list of supported countries. " +
"%1$@ is a placeholder for the localized list of supported country names."
)
return String.localizedStringWithFormat(format, formattedCountryList)
case let .unsupportedCurrency(supportedCurrencies):
let currencyList = supportedCurrencies.map { $0.rawValue }
let formattedCurrencyList = ListFormatter.localizedString(byJoining: currencyList)
let format = NSLocalizedString(
"pos.ineligible.suggestion.unsupportedCurrency",
value: "The POS system is not available for your store’s currency. It currently supports only %1$@. " +
"Please check your store currency settings or contact support for assistance.",
comment: "Suggestion for unsupported currency with list of supported currencies. " +
"%1$@ is a placeholder for the localized list of supported currency codes."
)
return String.localizedStringWithFormat(format, formattedCurrencyList)
case .siteSettingsNotAvailable:
return NSLocalizedString("pos.ineligible.reason.siteSettingsNotAvailable",
value: "Unable to load store settings for POS.",
comment: "Ineligible reason: site settings unavailable")
return NSLocalizedString("pos.ineligible.suggestion.siteSettingsNotAvailable",
value: "Check your internet connection and try relaunching the app. If the issue persists, please contact support.",
comment: "Suggestion for site settings unavailable: check connection or contact support")
case .featureFlagDisabled:
return NSLocalizedString("pos.ineligible.reason.featureFlagDisabled",
value: "POS feature is currently disabled.",
comment: "Ineligible reason: feature flag disabled")
return NSLocalizedString("pos.ineligible.suggestion.featureFlagDisabled",
value: "POS is currently disabled.",
comment: "Suggestion for disabled feature flag: notify that POS is disabled remotely")
case .selfDeallocated:
return Localization.defaultReason
return NSLocalizedString("pos.ineligible.suggestion.selfDeallocated",
Copy link
Contributor

Choose a reason for hiding this comment

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

😬 hope we don't need this one!

value: "Try relaunching the app to resolve this issue.",
comment: "Suggestion for self deallocated: relaunch")
}
}
}

@available(iOS 17.0, *)
private extension POSIneligibleView {
enum Localization {
static let title = NSLocalizedString(
"pos.ineligible.title",
value: "Unable to load",
comment: "Title shown in POS ineligible view"
)

static let refreshEligibility = NSLocalizedString(
"pos.ineligible.refresh.button.title",
value: "Check Eligibility Again",
value: "Retry",
comment: "Button title to refresh POS eligibility check"
)

/// Default message shown when POS eligibility reason is not available.
static let defaultReason = NSLocalizedString(
"pos.ineligible.default.reason",
value: "Your store is not eligible for POS at this time.",
comment: "Default message shown when POS eligibility reason is not available"
static let dismiss = NSLocalizedString(
"pos.ineligible.dismiss.button.title",
value: "Exit POS",
comment: "Button title to dismiss POS ineligible view"
)
}
}

#if DEBUG

#Preview("Unsupported currency") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .unsupportedCurrency(supportedCurrencies: [.USD]),
onRefresh: {}
)
}
}

#Preview("Unsupported country") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .unsupportedCountry(supportedCountries: [.US, .GB]),
onRefresh: {}
)
}
}

#Preview {
POSIneligibleView(
reason: .unsupportedCurrency,
onRefresh: {}
)
#Preview("Not a tablet") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .notTablet,
onRefresh: {}
)
}
}

#Preview("Unsupported iOS version") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .unsupportedIOSVersion,
onRefresh: {}
)
}
}

#Preview("WooCommerce plugin not found") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .wooCommercePluginNotFound,
onRefresh: {}
)
}
}

#Preview("Feature flag disabled") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .featureFlagDisabled,
onRefresh: {}
)
}
}

#Preview("Feature switch disabled") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .featureSwitchDisabled,
onRefresh: {}
)
}
}

#Preview("Site settings unavailable") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .siteSettingsNotAvailable,
onRefresh: {}
)
}
}

#Preview("Feature switch sync failure") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .featureSwitchSyncFailure,
onRefresh: {}
)
}
}

#Preview("Unsupported WooCommerce version") {
if #available(iOS 17.0, *) {
POSIneligibleView(
reason: .unsupportedWooCommerceVersion(minimumVersion: "9.6.0"),
onRefresh: {}
)
}
}

#endif
Loading
Loading