diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosCatalogSyncOverdueBanner.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosCatalogSyncOverdueBanner.kt new file mode 100644 index 00000000000..f212fe123ea --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosCatalogSyncOverdueBanner.kt @@ -0,0 +1,113 @@ +package com.woocommerce.android.ui.woopos.home.items + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.scaleIn +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.woocommerce.android.R +import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview +import com.woocommerce.android.ui.woopos.common.composeui.component.ShadowType +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosCard +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.WooPosElevation +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 + +@Composable +fun WooPosCatalogSyncOverdueBanner( + state: WooPosItemsViewModel.CatalogSyncOverdueBannerState, + modifier: Modifier = Modifier, + onDismiss: () -> Unit +) { + AnimatedVisibility( + visible = state is WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible, + enter = fadeIn( + animationSpec = tween(durationMillis = 180) + ) + scaleIn( + animationSpec = tween(durationMillis = 180) + ), + modifier = modifier.padding(horizontal = WooPosSpacing.Small.value) + ) { + WooPosCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(WooPosCornerRadius.Medium.value), + backgroundColor = MaterialTheme.colorScheme.surfaceContainerLow, + elevation = WooPosElevation.Medium, + shadowType = ShadowType.Soft, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(WooPosSpacing.Medium.value), + modifier = Modifier + .fillMaxWidth() + .padding(WooPosSpacing.Medium.value), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_woo_pos_info_banner), + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = null, + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterVertically) + ) + Column( + verticalArrangement = Arrangement.spacedBy(WooPosSpacing.Small.value), + horizontalAlignment = Alignment.Start, + modifier = Modifier.weight(1f) + ) { + WooPosText( + text = stringResource(R.string.woopos_refresh_catalog_banner_title), + style = WooPosTypography.BodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + WooPosText( + text = stringResource(R.string.woopos_refresh_catalog_banner_message), + style = WooPosTypography.BodySmall, + color = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton( + onClick = onDismiss, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.woopos_refresh_catalog_banner_dismiss), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(24.dp) + ) + } + } + } + } +} + +@Composable +@WooPosPreview +fun WooPosRefreshCatalogBannerPreview() { + WooPosTheme { + WooPosCatalogSyncOverdueBanner( + state = WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible, + onDismiss = {} + ) + } +} 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 b3cb489ce07..0ae398220ca 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 @@ -5,8 +5,10 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyListState @@ -45,6 +47,7 @@ fun WooPosItemsScreen(modifier: Modifier = Modifier) { WooPosItemsScreen( modifier = modifier, itemsStateFlow = productsViewModel.viewState, + catalogSyncOverdueBannerStateFlow = productsViewModel.catalogSyncOverdueBannerState, productsViewState = productsViewState, couponsListState = couponsListState, onUIEvent = { productsViewModel.onUIEvent(it) }, @@ -56,15 +59,18 @@ fun WooPosItemsScreen(modifier: Modifier = Modifier) { private fun WooPosItemsScreen( modifier: Modifier = Modifier, itemsStateFlow: StateFlow, + catalogSyncOverdueBannerStateFlow: StateFlow, productsViewState: LazyListState, couponsListState: LazyListState, onUIEvent: (WooPosItemsUIEvent) -> Unit, ) { val state = itemsStateFlow.collectAsState() + val catalogSyncOverdueBannerState = catalogSyncOverdueBannerStateFlow.collectAsState() MainItemsList( modifier = modifier, state = state, + bannerState = catalogSyncOverdueBannerState, productsViewState = productsViewState, couponsListState = couponsListState, onSearchEvent = { @@ -86,6 +92,7 @@ private fun WooPosItemsScreen( }, onTabClicked = { onUIEvent(WooPosItemsUIEvent.OnTabClicked(it)) }, onBackClicked = { onUIEvent(WooPosItemsUIEvent.BackFromVariationsClicked) }, + onSyncWarningBannerDismissed = { onUIEvent(WooPosItemsUIEvent.SyncOverdueBannerDismissed) }, ) } @@ -94,20 +101,17 @@ private fun WooPosItemsScreen( private fun MainItemsList( modifier: Modifier, state: State, + bannerState: State, productsViewState: LazyListState, couponsListState: LazyListState, onSearchEvent: (WooPosSearchUIEvent) -> Unit, onTabClicked: (WooPosItemsToolbarViewState.Tab) -> Unit, onAddCouponEvent: () -> Unit, onBackClicked: () -> Unit, + onSyncWarningBannerDismissed: () -> Unit, ) { - Box( - modifier = modifier - .fillMaxSize() - ) { - Column( - modifier.fillMaxHeight() - ) { + Box(modifier = modifier.fillMaxSize()) { + Column(modifier.fillMaxHeight()) { WooPosItemsToolbar( modifier = Modifier .statusBarsPadding() @@ -121,6 +125,20 @@ private fun MainItemsList( onAddCouponEvent = onAddCouponEvent, ) + Spacer( + modifier = + Modifier + .height(WooPosSpacing.Small.value) + .padding(horizontal = WooPosSpacing.Medium.value.toAdaptivePadding()) + ) + + WooPosCatalogSyncOverdueBanner( + state = bannerState.value, + onDismiss = onSyncWarningBannerDismissed + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.Small.value)) + val currentState = state.value Crossfade( @@ -216,10 +234,12 @@ fun WooPosItemsScreenSearchVisiblePreview(modifier: Modifier = Modifier) { tabs = tabs() ) ) + val bannerState = MutableStateFlow(WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible) WooPosTheme { WooPosItemsScreen( modifier = modifier, itemsStateFlow = productState, + catalogSyncOverdueBannerStateFlow = bannerState, productsViewState = rememberLazyListState(), couponsListState = rememberLazyListState(), onUIEvent = {}, @@ -242,10 +262,12 @@ fun WooPosItemsScreenSearchHiddenPreview(modifier: Modifier = Modifier) { tabs = tabs() ) ) + val bannerState = MutableStateFlow(WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible) WooPosTheme { WooPosItemsScreen( modifier = modifier, itemsStateFlow = productState, + catalogSyncOverdueBannerStateFlow = bannerState, productsViewState = rememberLazyListState(), couponsListState = rememberLazyListState(), onUIEvent = {}, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsUIEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsUIEvent.kt index e8c23354756..39a7562caea 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsUIEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsUIEvent.kt @@ -12,4 +12,5 @@ sealed class WooPosItemsUIEvent { data object CloseSearchClicked : WooPosItemsUIEvent() data object SearchIconClicked : WooPosItemsUIEvent() data object AddCouponIconClicked : WooPosItemsUIEvent() + data object SyncOverdueBannerDismissed : WooPosItemsUIEvent() } 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 33b11d875f2..59c03eb84a1 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 @@ -12,6 +12,8 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState. import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState.Tab import com.woocommerce.android.ui.woopos.home.items.coupons.creation.WooPosCouponCreationFacade import com.woocommerce.android.ui.woopos.home.items.variations.WooPosVariationsNavigationData +import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncRequirement +import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncStatusChecker import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEventConstant @@ -35,6 +37,7 @@ class WooPosItemsViewModel @Inject constructor( private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver, private val preferencesRepository: WooPosPreferencesRepository, private val analyticsTracker: WooPosAnalyticsTracker, + private val syncStatusChecker: WooPosFullSyncStatusChecker, ) : ViewModel() { private var preservedStateBeforeOpeningVariations: WooPosItemsToolbarViewState? = null private val _viewState = MutableStateFlow(initialState()) @@ -45,6 +48,10 @@ class WooPosItemsViewModel @Inject constructor( initialValue = _viewState.value, ) + private val _catalogSyncOverdueBannerState = + MutableStateFlow(CatalogSyncOverdueBannerState.Hidden) + val catalogSyncOverdueBannerState: StateFlow = _catalogSyncOverdueBannerState + init { listenUpEvents() searchHelper.initialize( @@ -55,6 +62,18 @@ class WooPosItemsViewModel @Inject constructor( viewModelScope.launch { preferencesRepository.setWasOpenedOnce(true) } + + checkSyncStatusAndUpdateBanner() + } + + private fun checkSyncStatusAndUpdateBanner() { + viewModelScope.launch { + val requirement = syncStatusChecker.checkSyncRequirement() + _catalogSyncOverdueBannerState.value = when (requirement) { + is WooPosFullSyncRequirement.Overdue -> CatalogSyncOverdueBannerState.Visible + else -> CatalogSyncOverdueBannerState.Hidden + } + } } fun onUIEvent(event: WooPosItemsUIEvent) { @@ -77,6 +96,9 @@ class WooPosItemsViewModel @Inject constructor( } is WooPosItemsUIEvent.AddCouponIconClicked -> createAndAddCoupon() + WooPosItemsUIEvent.SyncOverdueBannerDismissed -> { + _catalogSyncOverdueBannerState.value = CatalogSyncOverdueBannerState.Hidden + } } } @@ -241,4 +263,9 @@ class WooPosItemsViewModel @Inject constructor( @Parcelize data class Coupon(override val id: Long, val couponCode: String) : ItemClickedData(id), Parcelable } + + sealed class CatalogSyncOverdueBannerState { + data object Hidden : CatalogSyncOverdueBannerState() + data object Visible : CatalogSyncOverdueBannerState() + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsScreen.kt index e58f4a08c85..93081d44378 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsScreen.kt @@ -2,7 +2,6 @@ package com.woocommerce.android.ui.woopos.home.items.coupons import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi @@ -22,7 +21,6 @@ import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorS import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorScreenButtonState import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosPaginationErrorIndicator 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.home.items.WooPosCouponsViewState import com.woocommerce.android.ui.woopos.home.items.WooPosItemList @@ -74,7 +72,6 @@ private fun WooPosCouponsScreen( when (val itemsState = state.value) { is WooPosCouponsViewState.Content -> { WooPosItemList( - modifier = Modifier.padding(top = WooPosSpacing.Large.value), state = itemsState, listState = listState, onItemClicked = { item -> onUIEvent(WooPosCouponsUIEvent.CouponClicked(item.id, item.name)) }, @@ -88,9 +85,7 @@ private fun WooPosCouponsScreen( } } - is WooPosCouponsViewState.Loading -> WooPosItemsLoadingIndicator( - modifier = Modifier.padding(top = WooPosSpacing.Large.value) - ) + is WooPosCouponsViewState.Loading -> WooPosItemsLoadingIndicator() is WooPosCouponsViewState.Empty -> WooPosEmptyScreen( modifier = Modifier.fillMaxSize(), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsScreen.kt index beae50958b3..5607f7705eb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsScreen.kt @@ -139,7 +139,6 @@ private fun Content( onEndOfItemListReached: () -> Unit ) { WooPosItemList( - modifier = Modifier.padding(top = WooPosSpacing.Large.value), state = itemsState, listState = listState, onItemClicked = onItemClicked, 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 17cf131b487..a0a339ec1c1 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 @@ -103,7 +103,6 @@ private fun WooPosVariationsScreens( when (val itemsState = itemState.value) { is WooPosVariationsViewState.Content -> { WooPosItemList( - modifier = Modifier.padding(top = WooPosSpacing.Large.value), state = itemsState, listState = listState, onItemClicked = { @@ -122,16 +121,12 @@ private fun WooPosVariationsScreens( } is WooPosVariationsViewState.Loading -> { - WooPosItemsLoadingIndicator( - modifier = Modifier.padding(top = WooPosSpacing.Large.value), - ) + WooPosItemsLoadingIndicator() } is WooPosVariationsViewState.Error -> { VariationsError( - modifier = Modifier - .width(640.dp) - .padding(top = WooPosSpacing.Large.value) + modifier = Modifier.width(640.dp) ) { onRetryClicked() } diff --git a/WooCommerce/src/main/res/drawable/ic_woo_pos_info_banner.xml b/WooCommerce/src/main/res/drawable/ic_woo_pos_info_banner.xml new file mode 100644 index 00000000000..af0dc4bf46c --- /dev/null +++ b/WooCommerce/src/main/res/drawable/ic_woo_pos_info_banner.xml @@ -0,0 +1,9 @@ + + + diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 12e2d39ab05..a9470e5b947 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3604,6 +3604,9 @@ Unable to sync We are unable to sync your product catalog. Please check your internet connection and retry. Retry + Refresh catalog + The catalog hasn\'t been synced in the last 7 days. Either connect your device to WiFi or enable syncing over cellular network in POS settings. + Dismiss banner Reader connected Connect your reader diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt index 72aa0356291..47868930d46 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt @@ -9,6 +9,7 @@ import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel.ItemClickedData import com.woocommerce.android.ui.woopos.home.items.coupons.creation.WooPosCouponCreationFacade +import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncStatusChecker import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped @@ -55,6 +56,7 @@ class WooPosItemsViewModelTest { private val fromChildToParentEventSender: WooPosChildrenToParentEventSender = mock() private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock() private val preferencesRepository: WooPosPreferencesRepository = mock() + private val syncStatusChecker: WooPosFullSyncStatusChecker = mock() @Before fun setup() { @@ -457,6 +459,7 @@ class WooPosItemsViewModelTest { parentToChildrenEventReceiver = parentToChildrenEventReceiver, preferencesRepository = preferencesRepository, analyticsTracker = analyticsTracker, + syncStatusChecker = syncStatusChecker, ) } }