Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.woocommerce.android.ui.woopos.common.data.WooPosVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosVariationMapper
import com.woocommerce.android.ui.woopos.common.data.getNameForPOS
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModel
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModelMapper
import com.woocommerce.android.ui.woopos.common.data.searchbyidentifier.WooPosSearchByIdentifier
import com.woocommerce.android.ui.woopos.common.data.searchbyidentifier.WooPosSearchByIdentifierResult
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
Expand Down Expand Up @@ -52,6 +53,7 @@ import com.woocommerce.android.viewmodel.getStateFlow
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.model.LocalOrRemoteId
import org.wordpress.android.fluxc.store.pos.localcatalog.WooPosLocalCatalogStore
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
Expand All @@ -75,6 +77,7 @@ class WooPosCartViewModel @Inject constructor(
private val soundHelper: WooPosSoundHelper,
private val barcodeEventTracker: WooPosBarcodeEventTracker,
private val variationMapper: WooPosVariationMapper,
private val productMapper: WooPosProductModelMapper,
private val wooPosLocalCatalogM1Enabled: WooPosLocalCatalogM1Enabled,
private val localCatalogStore: WooPosLocalCatalogStore,
private val site: SelectedSite,
Expand Down Expand Up @@ -339,13 +342,37 @@ class WooPosCartViewModel @Inject constructor(
}

private suspend fun handleSimpleProductClicked(productId: Long): WooPosCartItemViewState {
val product = getProductById(productId)!!
val product = when {
wooPosLocalCatalogM1Enabled() -> {
val result = localCatalogStore.getProduct(
siteId = LocalOrRemoteId.LocalId(site.get().id),
remoteProductId = LocalOrRemoteId.RemoteId(productId)
)
result.getOrNull()?.let { entity ->
productMapper.fromEntity(entity)
} ?: error("Product not found in local catalog: $productId")
}
else -> getProductById(productId)!!
}

val itemNumber = getItemNumber()
return product.toCartListItem(itemNumber)
}

private suspend fun handleVariationClicked(productId: Long, variationId: Long): WooPosCartItemViewState {
val product = getProductById(productId)!!
val product = when {
wooPosLocalCatalogM1Enabled() -> {
val result = localCatalogStore.getProduct(
siteId = LocalOrRemoteId.LocalId(site.get().id),
remoteProductId = LocalOrRemoteId.RemoteId(productId)
)
result.getOrNull()?.let { entity ->
productMapper.fromEntity(entity)
} ?: error("Product not found in local catalog: $productId")
}
else -> getProductById(productId)!!
}

val itemNumber = getItemNumber()

val productVariation = when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ class WooPosProductsDataSource @Inject constructor(
private val productsIndex: WooPosProductsIndex,
private val productsTypesFilterConfig: WooPosProductsTypesFilterConfig,
private val posProductMapper: WooPosWCProductToWooPosProductModelMapper,
) {
) : WooPosProductsDataSourceInterface {
private val canLoadMore = AtomicBoolean(false)
private val offset = AtomicInteger(0)

val hasMorePages: Boolean
override val hasMorePages: Boolean
get() = canLoadMore.get()

suspend fun prepopulateProductsCache(): Result<Unit> = coroutineScope {
Expand Down Expand Up @@ -80,11 +80,14 @@ class WooPosProductsDataSource @Inject constructor(
Result.success(Unit)
}

fun loadProducts(forceRefreshProducts: Boolean): Flow<ProductsResult> = flow {
override fun fetchFirstPage(
searchQuery: String?,
forceRefresh: Boolean
): Flow<ProductsResult> = flow {
offset.set(0)
productsIndex.clearCache()

if (!forceRefreshProducts) {
if (!forceRefresh) {
val cachedProducts = sortProducts(productsCache.getAll()).take(NORMAL_PAGE_SIZE)
emit(ProductsResult.Cached(cachedProducts))
}
Expand All @@ -98,7 +101,7 @@ class WooPosProductsDataSource @Inject constructor(
}
}.flowOn(Dispatchers.IO).take(2)

suspend fun loadMore(): Result<List<WooPosProductModel>> = withContext(Dispatchers.IO) {
override suspend fun loadMore(): Result<List<WooPosProductModel>> = withContext(Dispatchers.IO) {
if (!canLoadMore.get()) {
return@withContext Result.success(productsIndex.getProductList())
}
Expand Down Expand Up @@ -161,6 +164,11 @@ class WooPosProductsDataSource @Inject constructor(
data class Remote(val productsResult: Result<List<WooPosProductModel>>) : ProductsResult()
}

override suspend fun resetState() {
canLoadMore.set(false)
offset.set(0)
}

companion object {
private const val NORMAL_PAGE_SIZE = 25
private const val PRE_POPULATION_PAGE_SIZE = 100
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.woocommerce.android.ui.woopos.home.items.products

import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModel
import kotlinx.coroutines.flow.Flow

interface WooPosProductsDataSourceInterface {
fun fetchFirstPage(
searchQuery: String? = null,
forceRefresh: Boolean
): Flow<WooPosProductsDataSource.ProductsResult>

suspend fun loadMore(): Result<List<WooPosProductModel>>

val hasMorePages: Boolean

suspend fun resetState()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.woocommerce.android.ui.woopos.home.items.products

import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModel
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModelMapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.store.pos.localcatalog.WooPosLocalCatalogStore
import javax.inject.Inject

class WooPosProductsInDbDataSource @Inject constructor(
private val posLocalCatalogStore: WooPosLocalCatalogStore,
private val selectedSite: SelectedSite,
private val mapper: WooPosProductModelMapper
) : WooPosProductsDataSourceInterface {

private fun getProductsFromDatabaseFlow(): Flow<List<WooPosProductModel>> {
val siteModel = selectedSite.getOrNull() ?: return flow { emit(emptyList()) }
val siteId = org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId(siteModel.id)

return posLocalCatalogStore.observeProducts(siteId)
.map { result ->
result.getOrNull()?.map { entity ->
mapper.fromEntity(entity)
} ?: emptyList()
}
}

override fun fetchFirstPage(
searchQuery: String?,
forceRefresh: Boolean
): Flow<WooPosProductsDataSource.ProductsResult> = getProductsFromDatabaseFlow()
.map { products ->
val filteredProducts = if (searchQuery.isNullOrBlank()) {
products
} else {
products.filter { product ->
product.name.contains(searchQuery, ignoreCase = true) || product.sku.contains(
searchQuery,
ignoreCase = true
)
}
}
WooPosProductsDataSource.ProductsResult.Remote(Result.success(filteredProducts))
}
.flowOn(Dispatchers.IO)

override suspend fun loadMore(): Result<List<WooPosProductModel>> = withContext(Dispatchers.IO) {
Result.success(emptyList())
}

override val hasMorePages: Boolean = false

override suspend fun resetState() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import javax.inject.Inject

@HiltViewModel
class WooPosProductsViewModel @Inject constructor(
private val productsDataSource: WooPosProductsDataSource,
productsDataSource: WooPosProductsDataSource,
productsInDbDataSource: WooPosProductsInDbDataSource,
private val fromChildToParentEventSender: WooPosChildrenToParentEventSender,
private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver,
private val priceFormat: WooPosFormatPrice,
Expand All @@ -44,6 +45,12 @@ class WooPosProductsViewModel @Inject constructor(
private val selectedSite: SelectedSite,
private val resourceProvider: ResourceProvider,
) : ViewModel() {

private val currentDataSource: WooPosProductsDataSourceInterface = when (wooPosLocalCatalogM1Enabled()) {
true -> productsInDbDataSource
false -> productsDataSource
}

private var loadMoreProductsJob: Job? = null
private var loadProductsJob: Job? = null
private var loadMoreAfterLoadCompletes = false
Expand All @@ -61,6 +68,9 @@ class WooPosProductsViewModel @Inject constructor(

init {
listenEventsFromParent()
viewModelScope.launch {
currentDataSource.resetState()
}
loadProducts(
forceRefreshProducts = false,
withPullToRefresh = false,
Expand Down Expand Up @@ -181,7 +191,7 @@ class WooPosProductsViewModel @Inject constructor(
WooPosProductsViewState.Loading()
}

productsDataSource.loadProducts(forceRefreshProducts = forceRefreshProducts).collect { result ->
currentDataSource.fetchFirstPage(forceRefresh = forceRefreshProducts).collect { result ->
when (result) {
is WooPosProductsDataSource.ProductsResult.Cached -> {
if (result.products.isNotEmpty()) {
Expand All @@ -195,11 +205,12 @@ class WooPosProductsViewModel @Inject constructor(
val products = result.productsResult.getOrThrow()
if (products.isNotEmpty()) {
val currentState = _viewState.value
val paginationState = if (loadMoreProductsJob?.isActive == true) {
WooPosPaginationState.Loading
} else {
WooPosPaginationState.None
}
val paginationState =
if (loadMoreProductsJob?.isActive == true && currentDataSource.hasMorePages) {
WooPosPaginationState.Loading
} else {
WooPosPaginationState.None
}
if (currentState is WooPosProductsViewState.Content) {
currentState.copy(
items = products.map { it.toItemSelectionViewState() },
Expand Down Expand Up @@ -282,23 +293,25 @@ class WooPosProductsViewModel @Inject constructor(

if (loadProductsJob?.isActive == true) {
loadMoreAfterLoadCompletes = true
_viewState.value = currentState.copy(paginationState = WooPosPaginationState.Loading)
if (currentDataSource.hasMorePages) {
_viewState.value = currentState.copy(paginationState = WooPosPaginationState.Loading)
}
return
}

if (loadMoreProductsJob?.isActive == true) {
return
}

if (!productsDataSource.hasMorePages) {
if (!currentDataSource.hasMorePages) {
return
}

_viewState.value = currentState.copy(paginationState = WooPosPaginationState.Loading)

loadMoreProductsJob?.cancel()
loadMoreProductsJob = viewModelScope.launch {
val result = productsDataSource.loadMore()
val result = currentDataSource.loadMore()
_viewState.value = if (result.isSuccess) {
result.getOrThrow().toContentState().also {
analyticsTracker.track(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ import com.woocommerce.android.ui.woopos.common.data.WooPosGetVariationById
import com.woocommerce.android.ui.woopos.common.data.WooPosVariationMapper
import com.woocommerce.android.ui.woopos.common.data.getName
import com.woocommerce.android.ui.woopos.common.data.getNameForPOS
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModelMapper
import com.woocommerce.android.ui.woopos.featureflags.WooPosLocalCatalogM1Enabled
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.util.DateUtils
import com.woocommerce.android.viewmodel.ResourceProvider
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.model.LocalOrRemoteId
import org.wordpress.android.fluxc.store.WCOrderStore
import org.wordpress.android.fluxc.store.pos.localcatalog.WooPosLocalCatalogStore
import java.util.Date
import javax.inject.Inject

Expand All @@ -31,6 +35,9 @@ class WooPosTotalsRepository @Inject constructor(
private val orderMapper: OrderMapper,
private val resourceProvider: ResourceProvider,
private val variationMapper: WooPosVariationMapper,
private val productMapper: WooPosProductModelMapper,
private val wooPosLocalCatalogM1Enabled: WooPosLocalCatalogM1Enabled,
private val localCatalogStore: WooPosLocalCatalogStore,
) {
private var orderCreationJob: Deferred<Result<Order>>? = null

Expand Down Expand Up @@ -98,7 +105,18 @@ class WooPosTotalsRepository @Inject constructor(
quantity: Int,
itemData: WooPosItemsViewModel.ItemClickedData.Product.Simple
): Order.Item {
val productResult = getProductById(itemData.id)!!
val productResult = when {
wooPosLocalCatalogM1Enabled() -> {
val result = localCatalogStore.getProduct(
siteId = LocalOrRemoteId.LocalId(selectedSite.get().id),
remoteProductId = LocalOrRemoteId.RemoteId(itemData.id)
)
result.getOrNull()?.let { entity ->
productMapper.fromEntity(entity)
} ?: error("Product not found in local catalog: ${itemData.id}")
}
else -> getProductById(itemData.id)!!
}
return Order.Item.EMPTY.copy(
itemId = 0L,
productId = itemData.id,
Expand All @@ -115,7 +133,18 @@ class WooPosTotalsRepository @Inject constructor(
quantity: Int,
itemData: WooPosItemsViewModel.ItemClickedData.Product.Variation
): Order.Item {
val productResult = getProductById(itemData.productId)!!
val productResult = when {
wooPosLocalCatalogM1Enabled() -> {
val result = localCatalogStore.getProduct(
siteId = LocalOrRemoteId.LocalId(selectedSite.get().id),
remoteProductId = LocalOrRemoteId.RemoteId(itemData.productId)
)
result.getOrNull()?.let { entity ->
productMapper.fromEntity(entity)
} ?: error("Product not found in local catalog: ${itemData.productId}")
}
else -> getProductById(itemData.productId)!!
}
val variationResult = getVariationById(
productId = itemData.productId,
variationId = itemData.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.woocommerce.android.ui.woopos.common.data.WooPosGetVariationById
import com.woocommerce.android.ui.woopos.common.data.WooPosVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosVariationMapper
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModel
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModelMapper
import com.woocommerce.android.ui.woopos.common.data.searchbyidentifier.WooPosSearchByIdentifier
import com.woocommerce.android.ui.woopos.common.data.searchbyidentifier.WooPosSearchByIdentifierResult
import com.woocommerce.android.ui.woopos.common.data.toWooPosVariation
Expand Down Expand Up @@ -155,6 +156,7 @@ class WooPosCartViewModelTest {
private val searchByIdentifier: WooPosSearchByIdentifier = mock()
private val wooPosLogWrapper: WooPosLogWrapper = mock()
private val barcodeEventTracker: WooPosBarcodeEventTracker = mock()
private val productMapper: WooPosProductModelMapper = mock()
private val wooPosLocalCatalogM1Enabled: WooPosLocalCatalogM1Enabled = mock {
on { invoke() }.thenReturn(false)
}
Expand Down Expand Up @@ -1527,6 +1529,7 @@ class WooPosCartViewModelTest {
soundHelper,
barcodeEventTracker,
variationMapper,
productMapper,
wooPosLocalCatalogM1Enabled,
localCatalogStore,
selectedSite,
Expand Down
Loading