Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[Woo POS] Show POS Menu with WooCommerce Upgrade description When WooCommerce Version is Outdated #13807

Draft
wants to merge 13 commits into
base: trunk
Choose a base branch
from
Draft
Original file line number Diff line number Diff line change
@@ -479,6 +479,8 @@ class AnalyticsTracker private constructor(

// We have to call in non consistent way to match the iOS naming
const val VALUE_MORE_MENU_POS = "pointOfSale"
const val KEY_POS_NOT_ELIGIBLE_REASON = "not_eligible_reason"
const val VALUE_POS_OUTDATED_WOOCOMMERCE_VERSION = "outdated_woocommerce_version"

const val VALUE_MORE_MENU_PAYMENTS_BADGE_VISIBLE = "badge_visible"

Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ import com.woocommerce.android.ui.moremenu.MoreMenuEvent.NavigateToSubscriptions
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.NavigateToWooPosEvent
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.OpenBlazeCampaignCreationEvent
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.OpenBlazeCampaignListEvent
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.ShowWooPosWooCoreUpdateRequiredEvent
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.StartSitePickerEvent
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.ViewAdminEvent
import com.woocommerce.android.ui.moremenu.MoreMenuEvent.ViewCouponsEvent
@@ -42,6 +43,7 @@ import com.woocommerce.android.ui.woopos.root.WooPosActivity
import com.woocommerce.android.util.ChromeCustomTabUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.wordpress.android.util.ToastUtils
import javax.inject.Inject

@AndroidEntryPoint
@@ -113,6 +115,10 @@ class MoreMenuFragment : TopLevelFragment() {
is OpenBlazeCampaignCreationEvent -> openBlazeCreationFlow()
is OpenBlazeCampaignListEvent -> openBlazeCampaignList()
is NavigateToWooPosEvent -> openWooPos()
is ShowWooPosWooCoreUpdateRequiredEvent -> ToastUtils.showToast(
requireContext(),
R.string.more_menu_button_woo_pos_update_woocommerce_version_description
)
}
}
}
Original file line number Diff line number Diff line change
@@ -294,7 +294,7 @@ private fun MoreMenuSection(section: MoreMenuItemSection) {
section.items.forEach { item ->
when (item.state) {
MoreMenuItemButton.State.Loading -> MoreMenuLoading()
MoreMenuItemButton.State.Visible -> MoreMenuButton(item)
is MoreMenuItemButton.State.Visible -> MoreMenuButton(item)
MoreMenuItemButton.State.Hidden -> Unit
}
Spacer(modifier = Modifier.height(8.dp))
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ sealed class MoreMenuEvent : MultiLiveEvent.Event() {

data object ViewCustomersEvent : MoreMenuEvent()
data object NavigateToWooPosEvent : MoreMenuEvent()
data object ShowWooPosWooCoreUpdateRequiredEvent : MoreMenuEvent()
}

data class MoreMenuItemSection(
@@ -45,12 +46,17 @@ data class MoreMenuItemButton(
@StringRes val description: Int,
@DrawableRes val icon: Int,
@DrawableRes val extraIcon: Int? = null,
val state: State = State.Visible,
val state: State = State.Visible.Enabled,
val badgeState: BadgeState? = null,
val onClick: () -> Unit = {},
) {
enum class State {
Loading, Visible, Hidden,
sealed class State {
data object Loading : State()
sealed class Visible : State() {
data object Enabled : Visible()
data object WooCoreVersionNotSupported : Visible()
}
data object Hidden : State()
}

enum class Type {
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CAMPAIGN_LIST_ENTR
import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_ENTRY_POINT_DISPLAYED
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.KEY_OPTION
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.KEY_POS_NOT_ELIGIBLE_REASON
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_MENU_ADMIN_MENU
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_MENU_COUPONS
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_MENU_CUSTOMERS
@@ -19,6 +20,7 @@ import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_M
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_MENU_REVIEWS
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_MENU_UPGRADES
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_MORE_MENU_VIEW_STORE
import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_POS_OUTDATED_WOOCOMMERCE_VERSION
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper
import com.woocommerce.android.extensions.adminUrlOrDefault
import com.woocommerce.android.notifications.UnseenReviewsCountHandler
@@ -35,6 +37,7 @@ import com.woocommerce.android.ui.payments.taptopay.isAvailable
import com.woocommerce.android.ui.plans.domain.SitePlan
import com.woocommerce.android.ui.plans.repository.SitePlanRepository
import com.woocommerce.android.ui.woopos.WooPosIsEnabled
import com.woocommerce.android.util.GetWooCorePluginCachedVersion
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.ScopedViewModel
@@ -71,7 +74,8 @@ class MoreMenuViewModel @Inject constructor(
private val isGoogleForWooEnabled: IsGoogleForWooEnabled,
private val hasGoogleAdsCampaigns: HasGoogleAdsCampaigns,
private val isWooPosEnabled: WooPosIsEnabled,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
private val getWooCoreVersion: GetWooCorePluginCachedVersion,
) : ScopedViewModel(savedState) {
private var storeHasGoogleAdsCampaigns = false

@@ -114,8 +118,8 @@ class MoreMenuViewModel @Inject constructor(
buttonsStates: Map<MoreMenuItemButton.Type, MoreMenuItemButton.State>,
count: Int,
paymentsFeatureWasClicked: Boolean
) = listOf(
generatePOSSection(buttonsStates[MoreMenuItemButton.Type.WooPos]!!),
) = listOfNotNull(
generatePOSMenuButtons(buttonsStates),
generateSettingsMenuButtons(buttonsStates[MoreMenuItemButton.Type.Settings]!!),
generateGeneralSection(
unseenReviewsCount = count,
@@ -126,6 +130,31 @@ class MoreMenuViewModel @Inject constructor(
)
)

private fun generatePOSMenuButtons(
buttonsStates: Map<MoreMenuItemButton.Type, MoreMenuItemButton.State>
): MoreMenuItemSection? {
return buttonsStates[MoreMenuItemButton.Type.WooPos]?.let { wooPosState ->
when (wooPosState) {
is MoreMenuItemButton.State.Loading, is MoreMenuItemButton.State.Visible.Enabled ->
generateWooPosSection(
wooPosState,
R.string.more_menu_button_woo_pos_description
) {
onWooPosButtonClick()
}

is MoreMenuItemButton.State.Visible.WooCoreVersionNotSupported -> generateWooPosSection(
wooPosState,
R.string.more_menu_button_woo_pos_update_woocommerce_version_description
) {
onWooPOSNotEligibleButtonClick(WooPosNotEligible.OutdatedWooCommerceVersion)
}

else -> null
}
}
}

fun onViewResumed() {
moreMenuNewFeatureHandler.markNewFeatureAsSeen()
launch {
@@ -134,20 +163,25 @@ class MoreMenuViewModel @Inject constructor(
}
}

private fun generatePOSSection(wooPosState: MoreMenuItemButton.State) =
MoreMenuItemSection(
private fun generateWooPosSection(
wooPosState: MoreMenuItemButton.State,
description: Int,
onPosButtonClick: () -> Unit
): MoreMenuItemSection {
return MoreMenuItemSection(
title = null,
items = listOf(
MoreMenuItemButton(
title = R.string.more_menu_button_woo_pos,
description = R.string.more_menu_button_woo_pos_description,
description = description,
icon = R.drawable.ic_more_menu_pos,
extraIcon = R.drawable.ic_more_menu_pos_extra,
state = wooPosState,
onClick = ::onWooPosButtonClick,
onClick = onPosButtonClick,
)
)
)
}

@Suppress("LongMethod")
private fun generateGeneralSection(
@@ -400,6 +434,20 @@ class MoreMenuViewModel @Inject constructor(
triggerEvent(MoreMenuEvent.NavigateToWooPosEvent)
}

private fun onWooPOSNotEligibleButtonClick(reason: WooPosNotEligible) {
when (reason) {
WooPosNotEligible.OutdatedWooCommerceVersion -> {
trackMoreMenuOptionSelected(
VALUE_MORE_MENU_POS,
extraOptions = mapOf(
KEY_POS_NOT_ELIGIBLE_REASON to VALUE_POS_OUTDATED_WOOCOMMERCE_VERSION
)
)
triggerEvent(MoreMenuEvent.ShowWooPosWooCoreUpdateRequiredEvent)
}
}
}

private fun onViewAdminButtonClick() {
trackMoreMenuOptionSelected(VALUE_MORE_MENU_ADMIN_MENU)
triggerEvent(MoreMenuEvent.ViewAdminEvent(selectedSite.get().adminUrlOrDefault))
@@ -460,16 +508,54 @@ class MoreMenuViewModel @Inject constructor(
.onStart { emit("") }

private fun checkFeaturesAvailability(): Flow<Map<MoreMenuItemButton.Type, MoreMenuItemButton.State>> {
val initialState = MoreMenuItemButton.Type.entries.associateWith {
MoreMenuItemButton.State.Loading
}.toMutableMap()
val initialState =
MoreMenuItemButton.Type.entries.associateWith<MoreMenuItemButton.Type, MoreMenuItemButton.State> {
MoreMenuItemButton.State.Loading
}.toMutableMap()

return listOf(
doCheckAvailability(MoreMenuItemButton.Type.Blaze) { isBlazeEnabled() },
doCheckAvailability(MoreMenuItemButton.Type.GoogleForWoo) { isGoogleForWooEnabled() },
doCheckAvailability(MoreMenuItemButton.Type.Inbox) { moreMenuRepository.isInboxEnabled() },
doCheckAvailability(MoreMenuItemButton.Type.Settings) { moreMenuRepository.isUpgradesEnabled() },
doCheckAvailability(MoreMenuItemButton.Type.WooPos) { isWooPosEnabled() }
doCheckAvailability(MoreMenuItemButton.Type.Blaze) {
if (isBlazeEnabled()) {
MoreMenuItemButton.State.Visible.Enabled
} else {
MoreMenuItemButton.State.Hidden
}
},
doCheckAvailability(MoreMenuItemButton.Type.GoogleForWoo) {
if (isGoogleForWooEnabled()) {
MoreMenuItemButton.State.Visible.Enabled
} else {
MoreMenuItemButton.State.Hidden
}
},
doCheckAvailability(MoreMenuItemButton.Type.Inbox) {
if (moreMenuRepository.isInboxEnabled()) {
MoreMenuItemButton.State.Visible.Enabled
} else {
MoreMenuItemButton.State.Hidden
}
},
doCheckAvailability(MoreMenuItemButton.Type.Settings) {
if (moreMenuRepository.isUpgradesEnabled()) {
MoreMenuItemButton.State.Visible.Enabled
} else {
MoreMenuItemButton.State.Hidden
}
},
doCheckAvailability(MoreMenuItemButton.Type.WooPos) {
when (isWooPosEnabled()) {
is WooPosIsEnabled.Reason.Disabled.CountryCurrencyNotSupported,
WooPosIsEnabled.Reason.Disabled.FeatureFlagDisabled,
WooPosIsEnabled.Reason.Disabled.InvalidSelectedSite,
WooPosIsEnabled.Reason.Disabled.InvalidSiteSettings,
WooPosIsEnabled.Reason.Disabled.ScreenSizeNotAllowed -> MoreMenuItemButton.State.Hidden

WooPosIsEnabled.Reason.Disabled.WooCoreVersionNotSupported ->
MoreMenuItemButton.State.Visible.WooCoreVersionNotSupported

WooPosIsEnabled.Reason.Enabled -> MoreMenuItemButton.State.Visible.Enabled
}
}
).merge()
.map { update ->
initialState[update.first] = update.second
@@ -480,12 +566,16 @@ class MoreMenuViewModel @Inject constructor(

private fun doCheckAvailability(
type: MoreMenuItemButton.Type,
checker: suspend () -> Boolean
checker: suspend () -> MoreMenuItemButton.State
): Flow<Pair<MoreMenuItemButton.Type, MoreMenuItemButton.State>> = flow {
val state = if (checker()) MoreMenuItemButton.State.Visible else MoreMenuItemButton.State.Hidden
val state = checker()
emit(type to state)
}

private val SitePlan.formattedPlanName
get() = generateFormattedPlanName(resourceProvider)

sealed class WooPosNotEligible {
data object OutdatedWooCommerceVersion : WooPosNotEligible()
}
}
Original file line number Diff line number Diff line change
@@ -19,19 +19,31 @@ class WooPosIsEnabled @Inject constructor(
private val isRemoteFeatureFlagEnabled: IsRemoteFeatureFlagEnabled,
) {
@Suppress("ReturnCount")
suspend operator fun invoke(): Boolean = coroutineScope {
val selectedSite = selectedSite.getOrNull() ?: return@coroutineScope false
suspend operator fun invoke(): Reason = coroutineScope {
val selectedSite = selectedSite.getOrNull()
?: return@coroutineScope Reason.Disabled.InvalidSelectedSite

if (!isRemoteFeatureFlagEnabled(WOO_POS)) return@coroutineScope false
if (!isScreenSizeAllowed()) return@coroutineScope false
if (!isWooCoreSupportsOrderAutoDraftsAndExtraPaymentsProps()) return@coroutineScope false
if (!isRemoteFeatureFlagEnabled(WOO_POS)) return@coroutineScope Reason.Disabled.FeatureFlagDisabled
if (!isScreenSizeAllowed()) return@coroutineScope Reason.Disabled.ScreenSizeNotAllowed
if (!isWooCoreSupportsOrderAutoDraftsAndExtraPaymentsProps()) {
return@coroutineScope Reason.Disabled.WooCoreVersionNotSupported
}

val siteSettings = wooCommerceStore.getSiteSettings(selectedSite) ?: return@coroutineScope false
val siteSettings = wooCommerceStore.getSiteSettings(selectedSite)
?: return@coroutineScope Reason.Disabled.InvalidSiteSettings

return@coroutineScope isCountryAndCurrencySupported(
countryCode = siteSettings.countryCode,
currency = siteSettings.currencyCode
)
return@coroutineScope if (isCountryAndCurrencySupported(
countryCode = siteSettings.countryCode,
currency = siteSettings.currencyCode
)
) {
Reason.Enabled
} else {
Reason.Disabled.CountryCurrencyNotSupported(
country = siteSettings.countryCode,
currency = siteSettings.currencyCode
)
}
}

private fun isCountryAndCurrencySupported(countryCode: String, currency: String) =
@@ -47,4 +59,16 @@ class WooPosIsEnabled @Inject constructor(

val SUPPORTED_COUNTRY_CURRENCY_PAIRS = listOf("us" to "usd", "gb" to "gbp")
}

sealed class Reason {
data object Enabled : Reason()
sealed class Disabled : Reason() {
data object InvalidSelectedSite : Disabled()
data object InvalidSiteSettings : Disabled()
data object FeatureFlagDisabled : Disabled()
data object ScreenSizeNotAllowed : Disabled()
data class CountryCurrencyNotSupported(val country: String, val currency: String) : Disabled()
data object WooCoreVersionNotSupported : Disabled()
}
}
}
1 change: 1 addition & 0 deletions WooCommerce/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -3832,6 +3832,7 @@

<string name="more_menu_button_woo_pos">Point of Sale Mode</string>
<string name="more_menu_button_woo_pos_description">Accept payments at your physical store</string>
<string name="more_menu_button_woo_pos_update_woocommerce_version_description">Upgrade your site to the latest WooCommerce to use Point of Sale</string>

<string name="more_menu_customers_title">Customers</string>

Loading