diff --git a/WooCommerce/src/androidTest/assets/mocks/mappings/woopos/settings.json b/WooCommerce/src/androidTest/assets/mocks/mappings/woopos/settings.json new file mode 100644 index 00000000000..ecba4978ab4 --- /dev/null +++ b/WooCommerce/src/androidTest/assets/mocks/mappings/woopos/settings.json @@ -0,0 +1,28 @@ +{ + "request": { + "method": "GET", + "urlPathPattern": "/rest/v1.1/jetpack-blogs/161477129/rest-api/", + "queryParameters": { + "path": { + "equalTo": "/wc/v3/settings/products/&_method=get" + }, + "json": { + "equalTo": "true" + }, + "locale": { + "matches": "(.*)" + } + } + }, + "response": { + "status": 200, + "jsonBody": { + "data": { + } + }, + "headers": { + "Content-Type": "application/json", + "Connection": "keep-alive" + } + } +} diff --git a/WooCommerce/src/androidTest/assets/mocks/mappings/woopos/stripe_account.json b/WooCommerce/src/androidTest/assets/mocks/mappings/woopos/stripe_account.json new file mode 100644 index 00000000000..ffaafe992ea --- /dev/null +++ b/WooCommerce/src/androidTest/assets/mocks/mappings/woopos/stripe_account.json @@ -0,0 +1,32 @@ +{ + "request": { + "method": "GET", + "urlPathPattern": "/rest/v1.1/jetpack-blogs/161477129/rest-api/", + "queryParameters": { + "path": { + "equalTo": "/wc/v3/wc_stripe/account/summary/&_method=get" + }, + "json": { + "equalTo": "true" + }, + "locale": { + "matches": "(.*)" + } + } + }, + "response": { + "status": 200, + "jsonBody": { + "data": { + "country": "US", + "storeCurrencies": { + "default": "USD" + } + } + }, + "headers": { + "Content-Type": "application/json", + "Connection": "keep-alive" + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModel.kt index 1241bb7bd9a..2515e6e2047 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModel.kt @@ -61,7 +61,7 @@ class DashboardViewModel @Inject constructor( dashboardTransactionLauncher: DashboardTransactionLauncher, shouldShowPrivacyBanner: ShouldShowPrivacyBanner, dashboardRepository: DashboardRepository, - private val feedbackPrefs: FeedbackPrefs + private val feedbackPrefs: FeedbackPrefs, ) : ScopedViewModel(savedState) { companion object { private const val DAYS_TO_REDISPLAY_JP_BENEFITS_BANNER = 5 diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt index da101692c21..793085833be 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt @@ -28,12 +28,14 @@ import com.woocommerce.android.ui.plans.trial.DetermineTrialStatusBarState import com.woocommerce.android.ui.prefs.PrivacySettingsRepository import com.woocommerce.android.ui.prefs.RequestedAnalyticsValue import com.woocommerce.android.ui.whatsnew.FeatureAnnouncementRepository +import com.woocommerce.android.ui.woopos.IsWooPosEnabled import com.woocommerce.android.util.BuildConfigWrapper import com.woocommerce.android.util.WooLog import com.woocommerce.android.util.WooLog.T import com.woocommerce.android.viewmodel.MultiLiveEvent.Event import com.woocommerce.android.viewmodel.ScopedViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update @@ -57,11 +59,16 @@ class MainActivityViewModel @Inject constructor( moreMenuNewFeatureHandler: MoreMenuNewFeatureHandler, unseenReviewsCountHandler: UnseenReviewsCountHandler, determineTrialStatusBarState: DetermineTrialStatusBarState, + isWooPosEnabled: IsWooPosEnabled, ) : ScopedViewModel(savedState) { init { launch { featureAnnouncementRepository.getFeatureAnnouncements(fromCache = false) } + launch(IO) { + // cache Woo POS eligibility result as soon as possible so that it doesn't block UI + isWooPosEnabled() + } } val startDestination = if (selectedSite.exists()) R.id.dashboard else R.id.nav_graph_site_picker diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabled.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabled.kt index 9e7ef857473..3bac3fd3135 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabled.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabled.kt @@ -1,8 +1,39 @@ package com.woocommerce.android.ui.woopos -import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.payments.GetActivePaymentsPlugin +import com.woocommerce.android.util.IsWindowClassExpandedAndBigger +import org.wordpress.android.fluxc.store.WCInPersonPaymentsStore +import org.wordpress.android.fluxc.store.WCInPersonPaymentsStore.InPersonPaymentsPluginType.WOOCOMMERCE_PAYMENTS import javax.inject.Inject +import javax.inject.Singleton -class IsWooPosEnabled @Inject constructor() { - operator fun invoke() = FeatureFlag.WOO_POS.isEnabled() +@Singleton +class IsWooPosEnabled @Inject constructor( + private val selectedSite: SelectedSite, + private val ippStore: WCInPersonPaymentsStore, + private val getActivePaymentsPlugin: GetActivePaymentsPlugin, + private val isWindowSizeExpandedAndBigger: IsWindowClassExpandedAndBigger, + private val isWooPosFFEnabled: IsWooPosFFEnabled, +) { + private var cachedResult: Boolean? = null + + @Suppress("ReturnCount") + suspend operator fun invoke(): Boolean { + cachedResult?.let { return it } + + if (!isWooPosFFEnabled()) return false + + val selectedSite = selectedSite.getOrNull() ?: return false + val ippPlugin = getActivePaymentsPlugin() ?: return false + val paymentAccount = ippStore.loadAccount(ippPlugin, selectedSite).model ?: return false + val countryCode = paymentAccount.country + + return ( + countryCode.lowercase() == "us" && + ippPlugin == WOOCOMMERCE_PAYMENTS && + paymentAccount.storeCurrencies.default.lowercase() == "usd" && + isWindowSizeExpandedAndBigger() + ).also { cachedResult = it } + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosFFEnabled.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosFFEnabled.kt new file mode 100644 index 00000000000..1fae746cc87 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/IsWooPosFFEnabled.kt @@ -0,0 +1,10 @@ +package com.woocommerce.android.ui.woopos + +import com.woocommerce.android.util.FeatureFlag +import javax.inject.Inject + +class IsWooPosFFEnabled @Inject constructor() { + operator fun invoke(): Boolean { + return FeatureFlag.WOO_POS.isEnabled() + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/util/UiHelpers.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/util/UiHelpers.kt index 28f051b2bba..78e23ad6d92 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/util/UiHelpers.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/util/UiHelpers.kt @@ -89,3 +89,7 @@ object UiHelpers { class IsWindowClassLargeThanCompact @Inject constructor(val context: Context) { operator fun invoke() = context.windowSizeClass != WindowSizeClass.Compact } + +class IsWindowClassExpandedAndBigger @Inject constructor(val context: Context) { + operator fun invoke() = context.windowSizeClass == WindowSizeClass.ExpandedAndBigger +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModelTest.kt index b9291ec633e..e95e118cd4b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/DashboardViewModelTest.kt @@ -71,7 +71,7 @@ class DashboardViewModelTest : BaseUnitTest() { selectedSite = selectedSite, shouldShowPrivacyBanner = shouldShowPrivacyBanner, dashboardRepository = dashboardRepository, - feedbackPrefs = feedbackPrefs + feedbackPrefs = feedbackPrefs, ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/main/MainActivityViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/main/MainActivityViewModelTest.kt index 1c2e3c8b60f..f9abcd02f52 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/main/MainActivityViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/main/MainActivityViewModelTest.kt @@ -583,7 +583,8 @@ class MainActivityViewModelTest : BaseUnitTest() { unseenReviewsCountHandler = unseenReviewsCountHandler, determineTrialStatusBarState = mock { onBlocking { invoke(any()) } doReturn emptyFlow() - } + }, + isWooPosEnabled = mock(), ) ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModelTests.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModelTests.kt index e92b4c183bc..d98c86d078e 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModelTests.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModelTests.kt @@ -74,7 +74,7 @@ class MoreMenuViewModelTests : BaseUnitTest() { onBlocking { invoke() } doReturn true } private val isWooPosEnabled: IsWooPosEnabled = mock { - on { invoke() } doReturn true + onBlocking { invoke() } doReturn true } private val blazeCampaignsStore: BlazeCampaignsStore = mock() diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabledTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabledTest.kt new file mode 100644 index 00000000000..684827174f8 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/IsWooPosEnabledTest.kt @@ -0,0 +1,113 @@ +package com.woocommerce.android.ui.woopos + +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.payments.GetActivePaymentsPlugin +import com.woocommerce.android.util.IsWindowClassExpandedAndBigger +import com.woocommerce.android.viewmodel.BaseUnitTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.payments.inperson.WCPaymentAccountResult +import org.wordpress.android.fluxc.model.payments.inperson.WCPaymentAccountResult.WCPaymentAccountStatus.COMPLETE +import org.wordpress.android.fluxc.model.payments.inperson.WCPaymentAccountResult.WCPaymentAccountStatus.StoreCurrencies +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult +import org.wordpress.android.fluxc.store.WCInPersonPaymentsStore +import org.wordpress.android.fluxc.store.WCInPersonPaymentsStore.InPersonPaymentsPluginType.STRIPE +import org.wordpress.android.fluxc.store.WCInPersonPaymentsStore.InPersonPaymentsPluginType.WOOCOMMERCE_PAYMENTS +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class IsWooPosEnabledTest : BaseUnitTest() { + private val selectedSite: SelectedSite = mock() + private val ippStore: WCInPersonPaymentsStore = mock() + private val getActivePaymentsPlugin: GetActivePaymentsPlugin = mock() + private val isWindowSizeExpandedAndBigger: IsWindowClassExpandedAndBigger = mock() + private val isWooPosFFEnabled: IsWooPosFFEnabled = mock() + + private lateinit var sut: IsWooPosEnabled + + @Before + fun setup() = testBlocking { + whenever(selectedSite.getOrNull()).thenReturn(SiteModel()) + whenever(getActivePaymentsPlugin()).thenReturn(WOOCOMMERCE_PAYMENTS) + whenever(isWindowSizeExpandedAndBigger()).thenReturn(true) + whenever(ippStore.loadAccount(any(), any())).thenReturn(buildPaymentAccountResult()) + whenever(isWooPosFFEnabled()).thenReturn(true) + + sut = IsWooPosEnabled( + selectedSite = selectedSite, + ippStore = ippStore, + getActivePaymentsPlugin = getActivePaymentsPlugin, + isWindowSizeExpandedAndBigger = isWindowSizeExpandedAndBigger, + isWooPosFFEnabled = isWooPosFFEnabled, + ) + } + + @Test + fun `given store not in the US, then return false`() = testBlocking { + val result = buildPaymentAccountResult(countryCode = "CA") + whenever(ippStore.loadAccount(any(), any())).thenReturn(result) + assertFalse(sut()) + } + + @Test + fun `given not big enough screen, then return false`() = testBlocking { + whenever(isWindowSizeExpandedAndBigger()).thenReturn(false) + assertFalse(sut()) + } + + @Test + fun `given currency is not USD, then return false`() = testBlocking { + val result = buildPaymentAccountResult(defaultCurrency = "CAD") + whenever(ippStore.loadAccount(any(), any())).thenReturn(result) + assertFalse(sut()) + } + + @Test + fun `given ipp plugin is not enabled, then return false`() = testBlocking { + whenever(getActivePaymentsPlugin()).thenReturn(null) + assertFalse(sut()) + } + + @Test + fun `given ipp plugin is not woo payments, then return false`() = testBlocking { + whenever(getActivePaymentsPlugin()).thenReturn(STRIPE) + assertFalse(sut()) + } + + @Test + fun `given feature flag disabled, then return false`() = testBlocking { + whenever(isWooPosFFEnabled.invoke()).thenReturn(false) + assertFalse(sut()) + } + + @Test + fun `given big enough screen, woo payments enabled, USD currency and store in the US, then return true`() = testBlocking { + val result = buildPaymentAccountResult(defaultCurrency = "USD", countryCode = "US", status = COMPLETE) + whenever(ippStore.loadAccount(any(), any())).thenReturn(result) + assertTrue(sut()) + } + + private fun buildPaymentAccountResult( + status: WCPaymentAccountResult.WCPaymentAccountStatus = COMPLETE, + countryCode: String = "US", + defaultCurrency: String = "USD" + ) = WooResult( + WCPaymentAccountResult( + status, + hasPendingRequirements = false, + hasOverdueRequirements = false, + currentDeadline = null, + statementDescriptor = "", + storeCurrencies = StoreCurrencies(defaultCurrency, listOf(defaultCurrency)), + country = countryCode, + isLive = true, + testMode = false, + ) + ) +}