diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationListHandler.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationListHandler.kt index 30ca51f878c9..667883791349 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationListHandler.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationListHandler.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.ui.products.variations.selector import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.wordpress.android.fluxc.store.WCProductStore import javax.inject.Inject class VariationListHandler @Inject constructor(private val repository: VariationSelectorRepository) { @@ -26,24 +27,39 @@ class VariationListHandler @Inject constructor(private val repository: Variation return canLoadMore || (offset + PAGE_SIZE < numOfVariations) } - suspend fun fetchVariations(productId: Long, forceRefresh: Boolean = false): Result = mutex.withLock { + suspend fun fetchVariations( + productId: Long, + forceRefresh: Boolean = false, + filterOptions: Map? = null + ): Result = mutex.withLock { // Reset the offset offset = 0 if (forceRefresh) { - loadVariations(productId) + loadVariations(productId, filterOptions) } else { Result.success(Unit) } } - suspend fun loadMore(productId: Long): Result = mutex.withLock { + suspend fun loadMore( + productId: Long, + filterOptions: Map? = null + ): Result = mutex.withLock { if (!canLoadMore) return@withLock Result.success(Unit) - loadVariations(productId) + loadVariations(productId, filterOptions) } - private suspend fun loadVariations(productId: Long): Result { - return repository.fetchVariations(productId, offset, PAGE_SIZE).onSuccess { + private suspend fun loadVariations( + productId: Long, + filterOptions: Map? = null + ): Result { + return repository.fetchVariations( + productId, + offset, + PAGE_SIZE, + filterOptions + ).onSuccess { canLoadMore = it offset += PAGE_SIZE }.map { } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorRepository.kt index b73684a9f6d5..32a5c32da1cb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorRepository.kt @@ -28,9 +28,16 @@ class VariationSelectorRepository @Inject constructor( suspend fun fetchVariations( productId: Long, offset: Int, - pageSize: Int + pageSize: Int, + filterOptions: Map? = null ): Result { - return productStore.fetchProductVariations(selectedSite.get(), productId, offset, pageSize) + return productStore.fetchProductVariations( + selectedSite.get(), + productId, + offset, + pageSize, + filterOptions = filterOptions, + ) .let { result -> if (result.isError) { WooLog.w( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt index bf18b493d0dc..b823845cc52b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.ui.woopos.home.items import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -17,7 +19,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -29,12 +33,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -45,6 +52,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.WooPosCard import com.woocommerce.android.ui.woopos.common.composeui.WooPosTheme import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosLazyColumn import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosShimmerBox +import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding import com.woocommerce.android.ui.woopos.home.items.WooPosItem.SimpleProduct import com.woocommerce.android.ui.woopos.home.items.WooPosItem.VariableProduct import com.woocommerce.android.ui.woopos.home.items.WooPosItem.Variation @@ -325,6 +333,51 @@ fun ItemsLoadingItem() { } } +@Composable +fun ItemsEmptyList( + title: String, + message: String, + contentDescription: String, +) { + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + modifier = Modifier.size(104.dp), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_woo_pos_empty_products), + contentDescription = contentDescription, + ) + + Spacer(modifier = Modifier.height(40.dp.toAdaptivePadding())) + + Text( + text = title, + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding())) + + Text( + text = message, + style = MaterialTheme.typography.h5, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp.toAdaptivePadding())) + } + } +} + @Composable private fun InfiniteListHandler( listState: LazyListState, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsScreen.kt index 5a77c02c932d..db981ebb98ec 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsScreen.kt @@ -2,7 +2,6 @@ package com.woocommerce.android.ui.woopos.home.items import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,8 +16,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ContentAlpha import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon @@ -35,12 +32,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.woocommerce.android.R @@ -182,7 +176,11 @@ private fun MainItemsList( is WooPosItemsViewState.Loading -> ItemsLoadingIndicator() - is WooPosItemsViewState.Empty -> ProductsEmptyList() + is WooPosItemsViewState.Empty -> ItemsEmptyList( + title = stringResource(id = R.string.woopos_products_empty_list_title), + message = stringResource(id = R.string.woopos_products_empty_list_message), + contentDescription = stringResource(id = R.string.woopos_products_empty_list_image_description), + ) is WooPosItemsViewState.Error -> ProductsError { onRetryClicked() } } @@ -265,47 +263,6 @@ private fun SimpleProductsBanner( } } -@Composable -fun ProductsEmptyList() { - Box( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Image( - modifier = Modifier.size(104.dp), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_woo_pos_empty_products), - contentDescription = stringResource(id = R.string.woopos_products_empty_list_image_description), - ) - - Spacer(modifier = Modifier.height(40.dp.toAdaptivePadding())) - - Text( - text = stringResource(id = R.string.woopos_products_empty_list_title), - style = MaterialTheme.typography.h4, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), - ) - - Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding())) - - Text( - text = stringResource(id = R.string.woopos_products_empty_list_message), - style = MaterialTheme.typography.h5, - color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp.toAdaptivePadding())) - } - } -} - @Composable fun ProductsError(onRetryClicked: () -> Unit) { Box( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSource.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSource.kt index d81928eabf47..1389922362d5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSource.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSource.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.ui.woopos.home.items.products import com.woocommerce.android.model.Product import com.woocommerce.android.ui.products.ProductStatus +import com.woocommerce.android.ui.products.ProductType.VARIABLE import com.woocommerce.android.ui.products.selector.ProductListHandler import com.woocommerce.android.util.WooLog import kotlinx.coroutines.Dispatchers @@ -83,7 +84,7 @@ class WooPosProductsDataSource @Inject constructor( } private fun List.applyPosProductFilter() = this.filter { product -> - isProductHasAPrice(product) + isProductHasAPrice(product) || product.productType == VARIABLE } private fun isProductHasAPrice(product: Product) = diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsDataSource.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsDataSource.kt index ea7c91963c8d..4873f05fb0bd 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsDataSource.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsDataSource.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext +import org.wordpress.android.fluxc.store.WCProductStore import javax.inject.Inject import javax.inject.Singleton @@ -47,7 +48,13 @@ class WooPosVariationsDataSource @Inject constructor( emit(FetchResult.Cached(cachedVariations)) } - val result = handler.fetchVariations(productId, forceRefresh = true) + val result = handler.fetchVariations( + productId, + forceRefresh = true, + filterOptions = mapOf( + WCProductStore.VariationFilterOption.STATUS to "publish" + ) + ) if (result.isSuccess) { val remoteVariations = handler.getVariationsFlow(productId).firstOrNull()?.applyFilter() ?: emptyList() updateCache(productId, remoteVariations) @@ -64,7 +71,12 @@ class WooPosVariationsDataSource @Inject constructor( }.flowOn(Dispatchers.IO) suspend fun loadMore(productId: Long): Result> = withContext(Dispatchers.IO) { - val result = handler.loadMore(productId) + val result = handler.loadMore( + productId, + filterOptions = mapOf( + WCProductStore.VariationFilterOption.STATUS to VARIATION_STATUS_PUBLISH + ) + ) if (result.isSuccess) { val fetchedVariations = handler.getVariationsFlow(productId).first().applyFilter() Result.success(fetchedVariations) @@ -75,6 +87,10 @@ class WooPosVariationsDataSource @Inject constructor( ) } } + + companion object { + private const val VARIATION_STATUS_PUBLISH = "publish" + } } private fun Result.logFailure() { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt index 09385c52012e..f895651d978e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt @@ -39,6 +39,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.component.Button import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorScreen import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosPaginationErrorIndicator import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding +import com.woocommerce.android.ui.woopos.home.items.ItemsEmptyList import com.woocommerce.android.ui.woopos.home.items.ItemsLoadingIndicator import com.woocommerce.android.ui.woopos.home.items.WooPosItem import com.woocommerce.android.ui.woopos.home.items.WooPosItemList @@ -157,7 +158,15 @@ private fun WooPosVariationsScreens( } } - else -> {} + is WooPosVariationsViewState.Empty -> { + ItemsEmptyList( + title = stringResource(id = R.string.woopos_variations_empty_list_title), + message = stringResource(id = R.string.woopos_variations_empty_list_message), + contentDescription = stringResource( + id = R.string.woopos_variations_empty_list_image_description + ) + ) + } } } PullRefreshIndicator( diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 4b2aae8fc2e4..0a3978730ec9 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4321,7 +4321,7 @@ Cart is empty No products No supported products found - POS currently only supports simple and variable products – \ncreate one to get started. + POS currently only supports simple, variable, and virtual products – \ncreate one to get started. POS currently only supports simple products Error loading products Give it another go? @@ -4331,6 +4331,10 @@ A cash payment of %1$s was successfully made A card payment of %1$s was successfully made + No supported variations found + POS currently only supports simple, variable, and virtual products – \ncreate one to get started. + No variations + Reader not connected To process this payment, please connect your reader. Connect to reader diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt index 07debfa46f6d..fde8ee82abe9 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt @@ -120,6 +120,7 @@ object ProductTestUtils { amount: String = "10.00", isVirtual: Boolean = false, isDownloadable: Boolean = false, + isPurchasable: Boolean = true, ): ProductVariation { return WCProductVariationModel(2).apply { dateCreated = "2018-01-05T05:14:30Z" @@ -131,6 +132,7 @@ object ProductTestUtils { attributes = "" virtual = isVirtual downloadable = isDownloadable + purchasable = isPurchasable }.toAppModel().also { it.priceWithCurrency = "$10.00" } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosProductsDataSourceTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosProductsDataSourceTest.kt index 06dffa4505ff..3df4b48b6ce8 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosProductsDataSourceTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosProductsDataSourceTest.kt @@ -16,6 +16,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse @ExperimentalCoroutinesApi @@ -369,4 +370,41 @@ class WooPosProductsDataSourceTest { assertFalse(cachedResult.products.any { it.remoteId == 1L }) } + + @Test + fun `given remote products, when loadSimpleProducts called, then do not filter out variable products even if price is null `() = + runTest { + // GIVEN + whenever(handler.canLoadMore).thenReturn(AtomicBoolean(true)) + whenever(handler.productsFlow).thenReturn( + flowOf( + listOf( + ProductTestUtils.generateProduct( + productId = 1, + productName = "Product 1", + amount = "", + productType = "variable", + ), + ProductTestUtils.generateProduct( + productId = 2, + productName = "Product 2", + amount = "20.0", + productType = "simple", + isVirtual = false, + isDownloadable = false + ).copy(firstImageUrl = "https://test.com") + ) + ) + ) + whenever(handler.loadFromCacheAndFetch(any(), any(), any(), any(), any())).thenReturn(Result.success(Unit)) + val sut = WooPosProductsDataSource(handler) + + // WHEN + val flow = sut.loadSimpleProducts(forceRefreshProducts = false).toList() + + // THEN + val remoteResult = flow[1] as WooPosProductsDataSource.ProductsResult.Remote + + assertEquals(2, remoteResult.productsResult.getOrNull()?.size) + } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsDataSourceTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsDataSourceTest.kt index 812c7a8fef36..f0c4f75a3737 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsDataSourceTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsDataSourceTest.kt @@ -17,6 +17,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.store.WCProductStore import kotlin.test.Test @ExperimentalCoroutinesApi @@ -146,7 +147,13 @@ class WooPosVariationsDataSourceTest { sut.fetchFirstPage(productId, forceRefresh = true).first() - whenever(handler.fetchVariations(productId, forceRefresh = true)) + whenever( + handler.fetchVariations( + productId, + forceRefresh = true, + mapOf(WCProductStore.VariationFilterOption.STATUS to "publish") + ) + ) .thenReturn(Result.failure(exception)) // WHEN @@ -196,7 +203,9 @@ class WooPosVariationsDataSourceTest { whenever(handler.canLoadMore(5)).thenReturn(true) whenever(handler.getVariationsFlow(productId)).thenReturn(flowOf(sampleProducts)) val exception = Exception("Load more failed") - whenever(handler.loadMore(productId)).thenReturn(Result.failure(exception)) + whenever( + handler.loadMore(productId, mapOf(WCProductStore.VariationFilterOption.STATUS to "publish")), + ).thenReturn(Result.failure(exception)) whenever(variationsCache.get(productId)).thenReturn(sampleProducts) val sut = WooPosVariationsDataSource(handler, variationsCache) @@ -223,7 +232,11 @@ class WooPosVariationsDataSourceTest { whenever(handler.getVariationsFlow(productId)).thenReturn(flowOf(emptyList())) val exception = Exception("Remote load failed") whenever( - handler.fetchVariations(productId, forceRefresh = true) + handler.fetchVariations( + productId, + forceRefresh = true, + mapOf(WCProductStore.VariationFilterOption.STATUS to "publish") + ) ).thenReturn(Result.failure(exception)) whenever(variationsCache.get(productId)).thenReturn(emptyList()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69547e56710e..251e231119b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ stripe-terminal = '3.7.1' tinder-statemachine = '0.2.0' wiremock = '2.26.3' wordpress-aztec = 'v2.1.4' -wordpress-fluxc = '2.102.0' +wordpress-fluxc = 'trunk-0264533ce612f74e1ae6fcbaefeb69b252163774' wordpress-login = '1.19.0' wordpress-libaddressinput = '0.0.2' wordpress-mediapicker = '0.3.1'