Skip to content

Commit

Permalink
Merge pull request #13442 from woocommerce/13441-woo-pos-removal-anim…
Browse files Browse the repository at this point in the history
…ation-from-the-cart-is-missing

[Woo POS] Removal animation from the cart is missing
  • Loading branch information
samiuelson authored Feb 11, 2025
2 parents 46bb108 + 8f502a2 commit 81619ae
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 154 deletions.
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
-----
- [**] Improved accessibility support (handling of increased text size) in in-person payment flows [https://github.com/woocommerce/woocommerce-android/pull/13414]
- [*] Automatically select the first available order when filtering is active in the two-pane layout.[https://github.com/woocommerce/woocommerce-android/pull/13491]

- [*] [Internal] Removal animation of the items from the cart [https://github.com/woocommerce/woocommerce-android/pull/13442]

21.7
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -37,9 +35,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter
Expand Down Expand Up @@ -258,7 +254,7 @@ private fun CartBodyWithItems(
key = { item -> item.id.itemNumber }
) { item ->
ProductItem(
modifier = Modifier,
modifier = Modifier.animateItem(),
item = item,
canRemoveItems = areItemsRemovable,
onUIEvent = onUIEvent,
Expand Down Expand Up @@ -402,117 +398,91 @@ private fun ProductItem(
canRemoveItems: Boolean,
onUIEvent: (WooPosCartUIEvent) -> Unit,
) {
var hasAnimationStarted by remember { mutableStateOf(item.isAppearanceAnimationPlayed) }
LaunchedEffect(Unit) {
hasAnimationStarted = true
}

val cardElevation = 6.dp
val elevation by animateDpAsState(
targetValue = if (hasAnimationStarted) cardElevation else 0.dp,
animationSpec = tween(durationMillis = 150, delayMillis = 250),
label = "elevation"
)

val itemContentDescription = stringResource(
id = R.string.woopos_cart_item_content_description,
item.name,
item.price
)

LaunchedEffect(elevation) {
if (elevation == cardElevation) {
onUIEvent(WooPosCartUIEvent.OnCartItemAppearanceAnimationPlayed(item))
}
}

AnimatedVisibility(
visible = hasAnimationStarted,
enter = expandVertically(
animationSpec = tween(durationMillis = 200)
),
exit = shrinkVertically()
WooPosCard(
modifier = modifier
.height(96.dp)
.semantics { contentDescription = itemContentDescription },
elevation = 6.dp,
shadowType = ShadowType.Soft,
shape = RoundedCornerShape(8.dp),
) {
WooPosCard(
modifier = modifier
.height(96.dp)
.semantics { contentDescription = itemContentDescription },
elevation = elevation,
shadowType = ShadowType.Soft,
shape = RoundedCornerShape(8.dp),
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(item.imageUrl)
.crossfade(true)
.build(),
fallback = ColorPainter(WooPosTheme.colors.loadingSkeleton),
error = ColorPainter(WooPosTheme.colors.loadingSkeleton),
placeholder = ColorPainter(WooPosTheme.colors.loadingSkeleton),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(96.dp)
)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(item.imageUrl)
.crossfade(true)
.build(),
fallback = ColorPainter(WooPosTheme.colors.loadingSkeleton),
error = ColorPainter(WooPosTheme.colors.loadingSkeleton),
placeholder = ColorPainter(WooPosTheme.colors.loadingSkeleton),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(96.dp)
)

Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding()))
Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding()))

Column(
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = item.name,
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clearAndSetSemantics { }
)
Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding()))
if (item.description.isNotNullOrEmpty()) {
Text(
text = item.name,
text = item.description!!,
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
maxLines = 1,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clearAndSetSemantics { }
)
Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding()))
if (item.description.isNotNullOrEmpty()) {
Text(
text = item.description!!,
style = MaterialTheme.typography.body1,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.secondaryVariant,
modifier = Modifier.clearAndSetSemantics { }
)
Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding()))
}
Text(
text = item.price,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.secondaryVariant,
modifier = Modifier.clearAndSetSemantics { }
)
Spacer(modifier = Modifier.height(4.dp.toAdaptivePadding()))
}
Text(
text = item.price,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.secondaryVariant,
modifier = Modifier.clearAndSetSemantics { }
)
}

if (canRemoveItems) {
Spacer(modifier = Modifier.width(8.dp.toAdaptivePadding()))
if (canRemoveItems) {
Spacer(modifier = Modifier.width(8.dp.toAdaptivePadding()))

val removeButtonContentDescription = stringResource(
id = R.string.woopos_remove_item_button_from_cart_content_description,
item.name
val removeButtonContentDescription = stringResource(
id = R.string.woopos_remove_item_button_from_cart_content_description,
item.name
)
IconButton(
onClick = { onUIEvent(WooPosCartUIEvent.ItemRemovedFromCart(item)) },
modifier = Modifier
.size(32.dp)
.semantics { contentDescription = removeButtonContentDescription }
) {
Icon(
painter = painterResource(id = R.drawable.ic_pos_remove_cart_item),
tint = MaterialTheme.colors.onBackground,
contentDescription = null,
)
IconButton(
onClick = { onUIEvent(WooPosCartUIEvent.ItemRemovedFromCart(item)) },
modifier = Modifier
.size(32.dp)
.semantics { contentDescription = removeButtonContentDescription }
) {
Icon(
painter = painterResource(id = R.drawable.ic_pos_remove_cart_item),
tint = MaterialTheme.colors.onBackground,
contentDescription = null,
)
}
}
Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding()))
}
Spacer(modifier = Modifier.width(16.dp.toAdaptivePadding()))
}
}
}
Expand Down Expand Up @@ -542,7 +512,6 @@ fun WooPosCartScreenProductsPreview(modifier: Modifier = Modifier) {
"VW California VW California, VW California,VW California",
description = "test description",
price = "€50,000",
isAppearanceAnimationPlayed = true,
productType = ProductType.Simple,
),
WooPosCartState.Body.WithItems.Item(
Expand All @@ -556,7 +525,6 @@ fun WooPosCartScreenProductsPreview(modifier: Modifier = Modifier) {
description = "test description test description test description test description" +
" test description test description test description test description test description",
price = "$150,000",
isAppearanceAnimationPlayed = true,
productType = ProductType.Simple,
),
WooPosCartState.Body.WithItems.Item(
Expand All @@ -569,7 +537,6 @@ fun WooPosCartScreenProductsPreview(modifier: Modifier = Modifier) {
name = "VW California",
description = "",
price = "€250,000",
isAppearanceAnimationPlayed = true,
productType = ProductType.Simple,
)
)
Expand Down Expand Up @@ -605,7 +572,6 @@ fun WooPosCartScreenCheckoutPreview(modifier: Modifier = Modifier) {
name = "VW California",
description = null,
price = "€50,000",
isAppearanceAnimationPlayed = true,
productType = ProductType.Simple,
),
WooPosCartState.Body.WithItems.Item(
Expand All @@ -618,7 +584,6 @@ fun WooPosCartScreenCheckoutPreview(modifier: Modifier = Modifier) {
name = "VW California",
description = null,
price = "$150,000",
isAppearanceAnimationPlayed = true,
productType = ProductType.Simple,
),
WooPosCartState.Body.WithItems.Item(
Expand All @@ -631,7 +596,6 @@ fun WooPosCartScreenCheckoutPreview(modifier: Modifier = Modifier) {
name = "VW California",
description = null,
price = "€250,000",
isAppearanceAnimationPlayed = true,
productType = ProductType.Simple,
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ data class WooPosCartState(
val price: String,
val description: String?,
val imageUrl: String?,
val isAppearanceAnimationPlayed: Boolean,
val productType: ProductType,
) : Parcelable {
@Parcelize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ sealed class WooPosCartUIEvent {
data class ItemRemovedFromCart(val item: WooPosCartState.Body.WithItems.Item) : WooPosCartUIEvent()
data object ClearAllClicked : WooPosCartUIEvent()
data object BackClicked : WooPosCartUIEvent()
data class OnCartItemAppearanceAnimationPlayed(val item: WooPosCartState.Body.WithItems.Item) : WooPosCartUIEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,6 @@ class WooPosCartViewModel @Inject constructor(
body = WooPosCartState.Body.Empty
)
}

is WooPosCartUIEvent.OnCartItemAppearanceAnimationPlayed -> {
val currentState = _state.value
val currentStateBody = currentState.body as? WooPosCartState.Body.WithItems ?: return
_state.value = currentState.copy(
body = currentStateBody.copy(
itemsInCart = currentState.body.itemsInCart.map {
if (it.id == event.item.id) it.copy(isAppearanceAnimationPlayed = true) else it
}
)
)
}
}
}

Expand Down Expand Up @@ -268,7 +256,6 @@ class WooPosCartViewModel @Inject constructor(
description = null,
price = formatPrice(price),
imageUrl = firstImageUrl,
isAppearanceAnimationPlayed = false,
productType = ProductType.Simple,
)

Expand All @@ -286,7 +273,6 @@ class WooPosCartViewModel @Inject constructor(
description = getNameForPOS(product, resourceProvider),
price = formatPrice(price),
imageUrl = image?.source,
isAppearanceAnimationPlayed = false,
productType = ProductType.Variation,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ class WooPosCartViewModelTest {
name = product.name,
price = "10.0$",
imageUrl = product.firstImageUrl,
isAppearanceAnimationPlayed = false,
productType = ProductType.Simple,
description = null,
)
Expand Down Expand Up @@ -321,7 +320,6 @@ class WooPosCartViewModelTest {
name = product1.name,
price = "10.0$",
imageUrl = product1.firstImageUrl,
isAppearanceAnimationPlayed = false,
productType = ProductType.Simple,
description = null,
)
Expand Down Expand Up @@ -423,7 +421,6 @@ class WooPosCartViewModelTest {
name = product.name,
price = "10.0$",
imageUrl = product.firstImageUrl,
isAppearanceAnimationPlayed = false,
productType = ProductType.Simple,
description = null,
)
Expand Down Expand Up @@ -499,40 +496,6 @@ class WooPosCartViewModelTest {
verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart)
}

@Test
fun `given non-empty cart, when OnCartItemAppearanceAnimationPlayed is received, then should update UI`() = runTest {
// GIVEN
val product = ProductTestUtils.generateProduct(
productId = 23L,
productName = "title",
amount = "10.0"
).copy(firstImageUrl = "url")

val parentToChildrenEventsMutableFlow = MutableSharedFlow<ParentToChildrenEvent>()
whenever(parentToChildrenEventReceiver.events).thenReturn(parentToChildrenEventsMutableFlow)
whenever(getProductById(eq(product.remoteId))).thenReturn(product)
val sut = createSut()
val states = sut.state.captureValues()

parentToChildrenEventsMutableFlow.emit(
ParentToChildrenEvent.ItemClickedInProductSelector(
WooPosItemsViewModel.ItemClickedData.SimpleProduct(
id = product.remoteId
)
)
)

// WHEN
val firstItem = (states.last().body as WooPosCartState.Body.WithItems).itemsInCart.first()
val updatedItem = firstItem.copy(isAppearanceAnimationPlayed = true)
sut.onUIEvent(WooPosCartUIEvent.OnCartItemAppearanceAnimationPlayed(updatedItem))

// THEN
val finalState = states.last()
val finalItem = (finalState.body as WooPosCartState.Body.WithItems).itemsInCart.first()
assertThat(finalItem.isAppearanceAnimationPlayed).isTrue
}

@Test
fun `when simple product added to cart, then should track analytics event with product type simple`() = runTest {
// GIVEN
Expand Down

0 comments on commit 81619ae

Please sign in to comment.