diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt index 6473fc323a50..365f4f9d27e0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt @@ -99,6 +99,10 @@ sealed class ChildToParentEvent { val discountAmount: BigDecimal, ) } + + sealed class SettingsEvent : ChildToParentEvent() { + data class ShowSyncErrorDialog(val errorMessage: String) : SettingsEvent() + } } interface WooPosChildrenToParentEventReceiver { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt index e2b82255d23a..f62af29de663 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt @@ -90,6 +90,10 @@ sealed class ParentToChildrenEvent { val discountAmount: BigDecimal, ) } + + sealed class SettingsEvent : ParentToChildrenEvent() { + data object RetrySyncRequested : SettingsEvent() + } } interface WooPosParentToChildrenEventReceiver { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt index 389462231202..2213390e7f29 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt @@ -250,6 +250,8 @@ class WooPosHomeViewModel @Inject constructor( ChildToParentEvent.RefreshProductList -> { sendEventToChildren(ParentToChildrenEvent.RefreshProductList) } + + is ChildToParentEvent.SettingsEvent -> Unit } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt index ca95ff8b448d..0c0871b987f5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt @@ -216,7 +216,8 @@ class WooPosCartViewModel @Inject constructor( ParentToChildrenEvent.SearchEvent.Finished, ParentToChildrenEvent.SearchEvent.Started, ParentToChildrenEvent.RefreshProductList, - is ParentToChildrenEvent.CouponsRemoved -> Unit + is ParentToChildrenEvent.CouponsRemoved, + is ParentToChildrenEvent.SettingsEvent -> Unit is ParentToChildrenEvent.CouponsValidationFailed -> { onCouponsValidationFails() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt index 33f421483774..c7552996cfe6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt @@ -68,6 +68,7 @@ class WooPosItemsSearchHelper @Inject constructor( is ParentToChildrenEvent.RemoveCouponsClicked -> Unit is ParentToChildrenEvent.CouponsValidationFailed -> Unit is ParentToChildrenEvent.OrderSuccessfullyPaid -> Unit + is ParentToChildrenEvent.SettingsEvent -> Unit } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModel.kt index 59c03eb84a15..8b0e7bba0dba 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModel.kt @@ -117,7 +117,8 @@ class WooPosItemsViewModel @Inject constructor( ParentToChildrenEvent.SearchEvent.Finished, is ParentToChildrenEvent.SearchEvent.RecentSearchSelected, ParentToChildrenEvent.SearchEvent.Started, - is ParentToChildrenEvent.BarcodeEvent -> Unit + is ParentToChildrenEvent.BarcodeEvent, + is ParentToChildrenEvent.SettingsEvent -> Unit is ParentToChildrenEvent.OrderSuccessfullyPaid -> _viewState.value = initialState() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsInDbDataSource.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsInDbDataSource.kt index 5cd0956eed65..b0b183c352df 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsInDbDataSource.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsInDbDataSource.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +import org.wordpress.android.fluxc.model.LocalOrRemoteId import org.wordpress.android.fluxc.store.pos.localcatalog.WooPosLocalCatalogStore import javax.inject.Inject @@ -20,7 +21,7 @@ class WooPosProductsInDbDataSource @Inject constructor( private fun getProductsFromDatabaseFlow(): Flow> { val siteModel = selectedSite.getOrNull() ?: return flow { emit(emptyList()) } - val siteId = org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId(siteModel.id) + val siteId = LocalOrRemoteId.LocalId(siteModel.id) return posLocalCatalogStore.observeProducts(siteId) .map { result -> diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt index 7669de1c284a..7295d5e51f0c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt @@ -91,7 +91,8 @@ class WooPosProductsViewModel @Inject constructor( is ParentToChildrenEvent.SearchEvent.ChangedQuery, ParentToChildrenEvent.SearchEvent.Finished, is ParentToChildrenEvent.SearchEvent.RecentSearchSelected, - ParentToChildrenEvent.SearchEvent.Started -> Unit + ParentToChildrenEvent.SearchEvent.Started, + is ParentToChildrenEvent.SettingsEvent -> Unit } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt index 1e01cd4123b3..c0b3b69b8d81 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt @@ -193,6 +193,7 @@ class WooPosItemsSearchViewModel @Inject constructor( is ParentToChildrenEvent.CouponsRemoved -> Unit is ParentToChildrenEvent.RefreshProductList -> Unit is ParentToChildrenEvent.CouponsValidationFailed -> Unit + is ParentToChildrenEvent.SettingsEvent -> Unit is ParentToChildrenEvent.ItemClickedInItemsList -> { if (event.itemData is ItemClickedData.Product.Variation && searchHelper.isSearchOpen()) { storeRecentSearch() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt index dd9b3c593940..2456b0400ff8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt @@ -325,7 +325,8 @@ class WooPosTotalsViewModel @Inject constructor( ParentToChildrenEvent.RemoveCouponsClicked, ParentToChildrenEvent.RefreshProductList, ParentToChildrenEvent.CouponsValidationFailed, - is ParentToChildrenEvent.BarcodeEvent -> Unit + is ParentToChildrenEvent.BarcodeEvent, + is ParentToChildrenEvent.SettingsEvent -> Unit } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsScreen.kt index b7506dbefad5..66cc10f97ff1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsScreen.kt @@ -22,6 +22,7 @@ import com.woocommerce.android.ui.woopos.scanningsetup.WooPosScanningSetupDialog import com.woocommerce.android.ui.woopos.settings.categories.WooPosSettingsCategoriesPaneScreen import com.woocommerce.android.ui.woopos.settings.categories.WooPosSettingsCategory import com.woocommerce.android.ui.woopos.settings.details.WooPosSettingsDetailPaneScreen +import com.woocommerce.android.ui.woopos.settings.details.localcatalog.WooPosSyncErrorDialog import com.woocommerce.android.ui.woopos.settings.productinfo.WooPosSettingsProductInfoDialog import com.woocommerce.android.ui.woopos.settings.productinfo.WooPosSettingsProductInfoDialogState @@ -49,6 +50,7 @@ fun WooPosSettingsScreen(onNavigationEvent: (WooPosNavigationEvent) -> Unit) { onBack = containerViewModel::navigateBack, onShowProductInfoDialog = containerViewModel::showProductInfoDialog, onShowScanningSetupDialog = containerViewModel::showScanningSetupDialog, + onRetrySync = containerViewModel::onRetrySyncFromDialogClicked, onDismissDialog = containerViewModel::hideDialog ) } @@ -62,6 +64,7 @@ private fun WooPosSettingsContent( onBack: () -> Unit, onShowProductInfoDialog: () -> Unit, onShowScanningSetupDialog: () -> Unit, + onRetrySync: () -> Unit, onDismissDialog: () -> Unit ) { Row( @@ -107,6 +110,12 @@ private fun WooPosSettingsContent( isVisible = dialogState is WooPosSettingsDialogState.ScanningSetupDialog, onDismissRequest = onDismissDialog ) + + WooPosSyncErrorDialog( + isVisible = dialogState is WooPosSettingsDialogState.SyncErrorDialog, + onRetry = onRetrySync, + onDismissRequest = onDismissDialog + ) } @WooPosPreview diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsState.kt index 11d0fcc963db..24d3d60bd54d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsState.kt @@ -76,4 +76,7 @@ sealed class WooPosSettingsDialogState : Parcelable { @Parcelize data object ScanningSetupDialog : WooPosSettingsDialogState() + + @Parcelize + data class SyncErrorDialog(val errorMessage: String) : WooPosSettingsDialogState() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModel.kt index 8027b4647bc3..957ca5e5e08c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModel.kt @@ -2,6 +2,10 @@ package com.woocommerce.android.ui.woopos.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.woocommerce.android.ui.woopos.home.ChildToParentEvent +import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent +import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventReceiver +import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventSender import com.woocommerce.android.ui.woopos.settings.categories.WooPosSettingsCategory import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.HardwareTapped @@ -21,11 +25,37 @@ import javax.inject.Inject @HiltViewModel class WooPosSettingsViewModel @Inject constructor( - private val analyticsTracker: WooPosAnalyticsTracker + private val analyticsTracker: WooPosAnalyticsTracker, + private val childToParentEventReceiver: WooPosChildrenToParentEventReceiver, + private val parentToChildEventSender: WooPosParentToChildrenEventSender, ) : ViewModel() { private val _state = MutableStateFlow(WooPosSettingsState()) val state: StateFlow = _state.asStateFlow() + init { + listenToChildEvents() + } + + private fun listenToChildEvents() { + viewModelScope.launch { + childToParentEventReceiver.events.collect { event -> + when (event) { + is ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog -> { + showSyncErrorDialog(event.errorMessage) + } + else -> Unit + } + } + } + } + + fun onRetrySyncFromDialogClicked() { + hideDialog() + viewModelScope.launch { + parentToChildEventSender.sendToChildren(ParentToChildrenEvent.SettingsEvent.RetrySyncRequested) + } + } + fun onCategorySelected(category: WooPosSettingsCategory) { trackCategorySelection(category) _state.update { currentState -> @@ -80,6 +110,12 @@ class WooPosSettingsViewModel @Inject constructor( } } + fun showSyncErrorDialog(errorMessage: String) { + _state.update { currentState -> + currentState.copy(dialogState = WooPosSettingsDialogState.SyncErrorDialog(errorMessage)) + } + } + fun hideDialog() { _state.update { currentState -> currentState.copy(dialogState = WooPosSettingsDialogState.Hidden) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogScreen.kt index 2b21a8a80f0e..f93d019dfd4e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogScreen.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ui.woopos.settings.details.localcatalog +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -9,10 +10,15 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults @@ -30,12 +36,16 @@ import com.woocommerce.android.R import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButtonState +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosDialogWrapper +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosOutlinedButton import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosShimmerBox import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosText import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosCornerRadius +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosIcons import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosSpacing import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTheme import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.toAdaptivePadding @Composable fun WooPosSettingsLocalCatalogScreen( @@ -54,10 +64,10 @@ fun WooPosSettingsLocalCatalogScreen( @Composable private fun WooPosSettingsLocalCatalogScreen( + modifier: Modifier = Modifier, state: WooPosSettingsLocalCatalogState, onToggleCellularData: (Boolean) -> Unit, - onRefreshCatalog: () -> Unit, - modifier: Modifier = Modifier + onRefreshCatalog: () -> Unit ) { Column( modifier = modifier @@ -274,6 +284,95 @@ private fun SectionTitle(title: String) { ) } +@Composable +fun WooPosSyncErrorDialog( + modifier: Modifier = Modifier, + isVisible: Boolean, + onRetry: () -> Unit, + onDismissRequest: () -> Unit +) { + WooPosDialogWrapper( + modifier = modifier, + isVisible = isVisible, + dialogBackgroundContentDescription = stringResource( + id = R.string.woopos_settings_local_catalog_sync_error_dialog_background_content_description + ), + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surfaceBright) + .padding(WooPosSpacing.XLarge.value.toAdaptivePadding()) + ) { + Row { + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = onDismissRequest, + modifier = Modifier + ) { + Icon( + Icons.Default.Close, + contentDescription = stringResource( + id = R.string.woopos_exit_dialog_confirmation_close_content_description + ), + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } + + Spacer(modifier = Modifier.size(WooPosSpacing.XLarge.value.toAdaptivePadding())) + + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = WooPosIcons.ErrorX, + contentDescription = null, + modifier = Modifier + .padding(WooPosSpacing.Medium.value.toAdaptivePadding()) + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.Large.value.toAdaptivePadding())) + + WooPosText( + text = stringResource(R.string.woopos_settings_local_catalog_sync_error_dialog_title), + style = WooPosTypography.Heading, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.Medium.value.toAdaptivePadding())) + + WooPosText( + text = stringResource(R.string.woopos_settings_local_catalog_sync_error_dialog_message), + style = WooPosTypography.BodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.XLarge.value.toAdaptivePadding())) + + WooPosButton( + modifier = Modifier.fillMaxWidth(), + onClick = onRetry, + text = stringResource(R.string.woopos_settings_local_catalog_sync_error_dialog_retry_button) + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.Medium.value.toAdaptivePadding())) + + WooPosOutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = onDismissRequest, + text = stringResource(R.string.woopos_settings_local_catalog_sync_error_dialog_cancel_button) + ) + } + } + } +} + @WooPosPreview @Composable fun WooPosSettingsLocalCatalogScreenPreview() { @@ -322,3 +421,15 @@ fun WooPosSettingsLocalCatalogRefreshingPreview() { ) } } + +@WooPosPreview +@Composable +fun WooPosSyncErrorDialogPreview() { + WooPosTheme { + WooPosSyncErrorDialog( + isVisible = true, + onRetry = {}, + onDismissRequest = {} + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModel.kt index 4ed0c10982b1..9c6712e5bcb6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModel.kt @@ -3,6 +3,10 @@ package com.woocommerce.android.ui.woopos.settings.details.localcatalog import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.woopos.home.ChildToParentEvent +import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent +import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender +import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.localcatalog.PosLocalCatalogSyncResult import com.woocommerce.android.ui.woopos.localcatalog.WooPosLocalCatalogSyncRepository import com.woocommerce.android.ui.woopos.localcatalog.WooPosLocalCatalogSyncScheduler @@ -25,6 +29,8 @@ class WooPosSettingsLocalCatalogViewModel @Inject constructor( private val dateFormatter: WooPosDateFormatter, private val preferencesRepository: WooPosPreferencesRepository, private val syncScheduler: WooPosLocalCatalogSyncScheduler, + private val childToParentEventSender: WooPosChildrenToParentEventSender, + private val parentToChildEventReceiver: WooPosParentToChildrenEventReceiver, ) : ViewModel() { private val _state = MutableStateFlow(WooPosSettingsLocalCatalogState()) val state: StateFlow = _state.asStateFlow() @@ -33,6 +39,21 @@ class WooPosSettingsLocalCatalogViewModel @Inject constructor( loadCatalogStatus() listenToCellularDataUpdateValue() + + listenToParentEvents() + } + + private fun listenToParentEvents() { + viewModelScope.launch { + parentToChildEventReceiver.events.collect { event -> + when (event) { + is ParentToChildrenEvent.SettingsEvent.RetrySyncRequested -> { + runFullCatalogSync() + } + else -> Unit + } + } + } } private fun loadCatalogStatus() { @@ -93,8 +114,10 @@ class WooPosSettingsLocalCatalogViewModel @Inject constructor( loadCatalogStatus() } is PosLocalCatalogSyncResult.Failure -> { - // TBD local catalog: Handle errors backupCatalogData?.let { _state.update { it.copy(catalogStatus = backupCatalogData) } } + childToParentEventSender.sendToParent( + ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog(result.error) + ) } } } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 09d76a8d235e..31959e37b615 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3667,6 +3667,11 @@ Manual Catalog Update Use this refresh only when something seems off - POS keeps data current automatically. Refresh Catalog + Sync error dialog background + Unable to sync catalog + Please check your internet connection and try again. + Retry + Cancel Never Just now diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModelTest.kt new file mode 100644 index 000000000000..a387f1f8233d --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/WooPosSettingsViewModelTest.kt @@ -0,0 +1,150 @@ +package com.woocommerce.android.ui.woopos.settings + +import com.woocommerce.android.ui.woopos.home.ChildToParentEvent +import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent +import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventReceiver +import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventSender +import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.argThat +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +class WooPosSettingsViewModelTest { + + @Rule + @JvmField + val coroutineTestRule = WooPosCoroutineTestRule() + + private val analyticsTracker: WooPosAnalyticsTracker = mock() + private val childToParentEventReceiver: WooPosChildrenToParentEventReceiver = mock() + private val parentToChildEventSender: WooPosParentToChildrenEventSender = mock() + + @Test + fun `when ShowSyncErrorDialog event collected, then dialog state is shown`() = runTest { + // GIVEN + val errorMessage = "Network error" + whenever(childToParentEventReceiver.events).thenReturn( + flowOf(ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog(errorMessage)) + ) + + // WHEN + val viewModel = createViewModel() + advanceUntilIdle() + + // THEN + assertThat(viewModel.state.value.dialogState) + .isInstanceOf(WooPosSettingsDialogState.SyncErrorDialog::class.java) + assertThat((viewModel.state.value.dialogState as WooPosSettingsDialogState.SyncErrorDialog).errorMessage) + .isEqualTo(errorMessage) + } + + @Test + fun `given sync error dialog shown, when hideDialog called, then dialog is hidden`() = runTest { + // GIVEN + val errorMessage = "Network error" + whenever(childToParentEventReceiver.events).thenReturn( + flowOf(ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog(errorMessage)) + ) + val viewModel = createViewModel() + advanceUntilIdle() + + // WHEN + viewModel.hideDialog() + + // THEN + assertThat(viewModel.state.value.dialogState).isEqualTo(WooPosSettingsDialogState.Hidden) + } + + @Test + fun `given sync error dialog shown, when retrySyncFromDialog called, then dialog is hidden`() = runTest { + // GIVEN + val errorMessage = "Network error" + whenever(childToParentEventReceiver.events).thenReturn( + flowOf(ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog(errorMessage)) + ) + val viewModel = createViewModel() + advanceUntilIdle() + + // WHEN + viewModel.onRetrySyncFromDialogClicked() + advanceUntilIdle() + + // THEN + assertThat(viewModel.state.value.dialogState).isEqualTo(WooPosSettingsDialogState.Hidden) + } + + @Test + fun `given sync error dialog shown, when retrySyncFromDialog called, then RetrySyncRequested event is sent`() = runTest { + // GIVEN + val errorMessage = "Network error" + whenever(childToParentEventReceiver.events).thenReturn( + flowOf(ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog(errorMessage)) + ) + val viewModel = createViewModel() + advanceUntilIdle() + + // WHEN + viewModel.onRetrySyncFromDialogClicked() + advanceUntilIdle() + + // THEN + verify(parentToChildEventSender).sendToChildren( + argThat { + this is ParentToChildrenEvent.SettingsEvent.RetrySyncRequested + } + ) + } + + @Test + fun `when default state is created, then dialog state is Hidden`() { + // GIVEN & WHEN + val initialState = WooPosSettingsState() + + // THEN + assertThat(initialState.dialogState).isEqualTo(WooPosSettingsDialogState.Hidden) + } + + @Test + fun `given multiple error events, when received, then dialog state is updated for each`() = runTest { + // GIVEN + val eventsFlow = MutableSharedFlow() + whenever(childToParentEventReceiver.events).thenReturn(eventsFlow) + + val viewModel = createViewModel() + advanceUntilIdle() + + // WHEN & THEN + eventsFlow.emit(ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog("Error 1")) + advanceUntilIdle() + assertThat(viewModel.state.value.dialogState) + .isInstanceOf(WooPosSettingsDialogState.SyncErrorDialog::class.java) + assertThat((viewModel.state.value.dialogState as WooPosSettingsDialogState.SyncErrorDialog).errorMessage) + .isEqualTo("Error 1") + + viewModel.hideDialog() + + eventsFlow.emit(ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog("Error 2")) + advanceUntilIdle() + assertThat((viewModel.state.value.dialogState as WooPosSettingsDialogState.SyncErrorDialog).errorMessage) + .isEqualTo("Error 2") + } + + private fun createViewModel(): WooPosSettingsViewModel { + return WooPosSettingsViewModel( + analyticsTracker = analyticsTracker, + childToParentEventReceiver = childToParentEventReceiver, + parentToChildEventSender = parentToChildEventSender, + ) + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModelTest.kt index c5bceace3ca5..531f6b14042e 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/settings/details/localcatalog/WooPosSettingsLocalCatalogViewModelTest.kt @@ -1,6 +1,10 @@ package com.woocommerce.android.ui.woopos.settings.details.localcatalog import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.woopos.home.ChildToParentEvent +import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent +import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender +import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.localcatalog.PosLocalCatalogSyncResult import com.woocommerce.android.ui.woopos.localcatalog.WooPosLocalCatalogSyncRepository import com.woocommerce.android.ui.woopos.localcatalog.WooPosLocalCatalogSyncScheduler @@ -9,6 +13,7 @@ import com.woocommerce.android.ui.woopos.util.datastore.WooPosPreferencesReposit import com.woocommerce.android.ui.woopos.util.datastore.WooPosSyncTimestampManager import com.woocommerce.android.ui.woopos.util.format.WooPosDateFormatter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -18,6 +23,7 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -37,9 +43,12 @@ class WooPosSettingsLocalCatalogViewModelTest { private val dateFormatter: WooPosDateFormatter = mock() private val preferencesRepository: WooPosPreferencesRepository = mock() private val syncScheduler: WooPosLocalCatalogSyncScheduler = mock() + private val childToParentEventSender: WooPosChildrenToParentEventSender = mock() + private val parentToChildEventReceiver: WooPosParentToChildrenEventReceiver = mock() private val mockSite: SiteModel = mock() private val allowCellularDataFlow = MutableStateFlow(false) + private val parentEventsFlow = MutableSharedFlow() private lateinit var sut: WooPosSettingsLocalCatalogViewModel @@ -47,6 +56,7 @@ class WooPosSettingsLocalCatalogViewModelTest { fun setUp() = runTest { whenever(selectedSite.get()).thenReturn(mockSite) whenever(preferencesRepository.allowCellularDataUpdate).thenReturn(allowCellularDataFlow) + whenever(parentToChildEventReceiver.events).thenReturn(parentEventsFlow) whenever(localCatalogSyncRepository.syncLocalCatalogFull(any())) .thenReturn( PosLocalCatalogSyncResult.Success( @@ -165,12 +175,129 @@ class WooPosSettingsLocalCatalogViewModelTest { verify(syncTimestampManager, times(2)).getFullSyncLastCompletedTimestamp() } + @Test + fun `given sync fails, when runFullCatalogSync called, then ShowSyncErrorDialog event is sent`() = runTest { + // GIVEN + val errorMessage = "Network error" + whenever(localCatalogSyncRepository.syncLocalCatalogFull(mockSite)) + .thenReturn(PosLocalCatalogSyncResult.Failure.UnexpectedError(errorMessage)) + + sut = createViewModel() + advanceUntilIdle() + + // WHEN + sut.runFullCatalogSync() + advanceUntilIdle() + + // THEN + verify(childToParentEventSender).sendToParent( + argThat { + this is ChildToParentEvent.SettingsEvent.ShowSyncErrorDialog && + this.errorMessage == errorMessage + } + ) + } + + @Test + fun `given sync fails, when runFullCatalogSync called, then catalog status is restored to previous state`() = runTest { + // GIVEN + val errorMessage = "Network error" + whenever(localCatalogSyncRepository.syncLocalCatalogFull(mockSite)) + .thenReturn(PosLocalCatalogSyncResult.Failure.UnexpectedError(errorMessage)) + + sut = createViewModel() + advanceUntilIdle() + + val initialStatus = sut.state.value.catalogStatus + + // WHEN + sut.runFullCatalogSync() + advanceUntilIdle() + + // THEN + assertThat(sut.state.value.catalogStatus).isEqualTo(initialStatus) + } + + @Test + fun `given sync succeeds, when runFullCatalogSync called, then catalog status is reloaded`() = runTest { + // GIVEN + whenever(localCatalogSyncRepository.syncLocalCatalogFull(mockSite)) + .thenReturn( + PosLocalCatalogSyncResult.Success( + productsSynced = 10, + variationsSynced = 5, + syncDurationMs = 1000 + ) + ) + + sut = createViewModel() + advanceUntilIdle() + + // WHEN + sut.runFullCatalogSync() + advanceUntilIdle() + + // THEN + assertThat(sut.state.value.catalogStatus) + .isInstanceOf(WooPosSettingsLocalCatalogState.CatalogStatus.Available::class.java) + } + + @Test + fun `given RetrySyncRequested event, when received from parent, then runFullCatalogSync is triggered`() = runTest { + // GIVEN + whenever(localCatalogSyncRepository.syncLocalCatalogFull(mockSite)) + .thenReturn( + PosLocalCatalogSyncResult.Success( + productsSynced = 10, + variationsSynced = 5, + syncDurationMs = 1000 + ) + ) + + sut = createViewModel() + advanceUntilIdle() + + // WHEN + parentEventsFlow.emit(ParentToChildrenEvent.SettingsEvent.RetrySyncRequested) + advanceUntilIdle() + + // THEN + assertThat(sut.state.value.catalogStatus) + .isInstanceOf(WooPosSettingsLocalCatalogState.CatalogStatus.Available::class.java) + } + + @Test + fun `when runFullCatalogSync called, then catalog status eventually updates to Available`() = runTest { + // GIVEN + whenever(localCatalogSyncRepository.syncLocalCatalogFull(mockSite)) + .thenReturn( + PosLocalCatalogSyncResult.Success( + productsSynced = 10, + variationsSynced = 5, + syncDurationMs = 1000 + ) + ) + + sut = createViewModel() + advanceUntilIdle() + + // WHEN + sut.runFullCatalogSync() + advanceUntilIdle() + + // THEN + assertThat(sut.state.value.catalogStatus) + .isInstanceOf(WooPosSettingsLocalCatalogState.CatalogStatus.Available::class.java) + } + private fun createViewModel() = WooPosSettingsLocalCatalogViewModel( syncTimestampManager = syncTimestampManager, localCatalogSyncRepository = localCatalogSyncRepository, selectedSite = selectedSite, dateFormatter = dateFormatter, preferencesRepository = preferencesRepository, - syncScheduler = syncScheduler + syncScheduler = syncScheduler, + childToParentEventSender = childToParentEventSender, + parentToChildEventReceiver = parentToChildEventReceiver, ) }