From a18cf7329f6f686aa68354ead7bf04c291999ba5 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Fri, 22 May 2026 12:44:09 +0200 Subject: [PATCH 1/5] WIP: Add coupons as line items to the cart --- .../snabble/sdk/shoppingcart/ShoppingCart.kt | 20 +++++++ .../sdk/shoppingcart/ShoppingCartUpdater.kt | 8 ++- .../cart/shoppingcart/ShoppingCartScreen.kt | 29 ++++++--- .../shoppingcart/ShoppingCartViewModel.kt | 50 ++++++++++++++-- .../shoppingcart/cartdiscount/CartDiscount.kt | 43 ++++++++------ .../widget/DiscountDescription.kt | 59 +++++++++---------- .../shoppingcart/product/DeletableProduct.kt | 6 +- .../product/model/DiscountItem.kt | 2 + .../shoppingcart/product/widget/Product.kt | 49 ++++++++++++++- 9 files changed, 198 insertions(+), 68 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt index 836bd62451..bd0dd5b5ca 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt @@ -17,6 +17,7 @@ import io.snabble.sdk.codes.ScannedCode import io.snabble.sdk.codes.templates.CodeTemplate import io.snabble.sdk.coupons.Coupon import io.snabble.sdk.coupons.CouponType +import io.snabble.sdk.extensions.xx import io.snabble.sdk.shoppingcart.data.Taxation import io.snabble.sdk.shoppingcart.data.cart.BackendCart import io.snabble.sdk.shoppingcart.data.cart.BackendCartCustomer @@ -216,6 +217,19 @@ class ShoppingCart( notifyItemRemoved(this, removedItem, index) } + /** + * Removed a cart item from the cart by its index + */ + fun removeDiscount(index: Int, discountId: String) { + data = data.copy(modCount = modCount.inc()) + generateNewUUID() + val removedItem = data.items.removeAt(index) + checkLimits() + updatePrices(debounce = size() != 0) + invalidateOnlinePrices() + notifyItemRemoved(this, removedItem, index) + } + /** * Removes a cart item from the cart by its id */ @@ -452,6 +466,12 @@ class ShoppingCart( if (index != -1) remove(index) } + fun removeCoupon(id: String) { + val asd = id.replace("_", "-") + val index = indexOfFirst { it?.coupon?.id.xx("cur") == asd.xx("remo")}.xx("index") + if (index != -1) remove(index) + } + /** * Gets a list of invalid products that were rejected by the backend. */ diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt index 910759e841..7fa9f8a3aa 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt @@ -201,7 +201,8 @@ internal class ShoppingCartUpdater( private fun addCartDiscounts(cartDiscountItems: List) { val totalCartDiscount = cartDiscountItems.sumOf { it.totalPrice } val cartDiscounts = cartDiscountItems.mapNotNull { it.name } - addCartDiscountLineItem(totalCartDiscount, cartDiscounts) + val discount = cartDiscountItems.firstOrNull{ it.discountRuleID != null} + addCartDiscountLineItem(totalCartDiscount, cartDiscounts,discount?.discountRuleID) } private fun deserializedCheckoutInfo( @@ -238,13 +239,14 @@ internal class ShoppingCartUpdater( } } - private fun addCartDiscountLineItem(discounts: Int, cartDiscounts: List) { + private fun addCartDiscountLineItem(discounts: Int, cartDiscounts: List, discountRuleId: String?) { if (discounts != 0) { val lineItem = LineItem( id = UUID.randomUUID().toString(), amount = 1, discountType = "cart", - name = cartDiscounts.first(), + name = cartDiscounts.firstOrNull(), + discountRuleID =discountRuleId, price = discounts, totalPrice = discounts, type = LineItemType.DISCOUNT diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt index 7287960c62..5c166c717b 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import io.snabble.sdk.Product import io.snabble.sdk.checkout.LineItem import io.snabble.sdk.shoppingcart.ShoppingCart import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.CartDiscount @@ -47,6 +46,9 @@ fun ShoppingCartScreen( }, onQuantityChanged = { item, quantity -> viewModel.onEvent(UpdateQuantity(item, quantity)) + }, + onDeleteDiscount = {item, discountId -> + viewModel.onEvent(DeleteDiscount(item, discountId)) } ) } @@ -57,7 +59,8 @@ private fun ShoppingCartScreen( uiState: UiState, modifier: Modifier = Modifier, onItemDeleted: (ShoppingCart.Item) -> Unit, - onQuantityChanged: (ShoppingCart.Item, Int) -> Unit + onQuantityChanged: (ShoppingCart.Item, Int) -> Unit, + onDeleteDiscount: (ShoppingCart.Item, String) -> Unit, ) { LazyColumn( @@ -72,6 +75,9 @@ private fun ShoppingCartScreen( onItemDeleted = { onItemDeleted(cartItem.item) }, onQuantityChanged = { quantity -> onQuantityChanged(cartItem.item, quantity) + }, + onDeleteDiscount = { + onDeleteDiscount(cartItem.item, it) } ) } @@ -84,7 +90,8 @@ private fun ShoppingCartScreen( is CartDiscountItem -> { CartDiscount( modifier = Modifier.fillMaxWidth(), - item = cartItem + item = cartItem, + onDelete = onItemDeleted ) } } @@ -128,7 +135,8 @@ private fun CardWithDefaultItems() { ShoppingCartScreen( uiState = uiState, onItemDeleted = {}, - onQuantityChanged = { _, _ -> } + onQuantityChanged = { _, _ -> }, + onDeleteDiscount = {_,_ ->} ) } @@ -146,9 +154,11 @@ private fun CardWithDiscountItem() { quantityText = "1", discounts = listOf( DiscountItem( + id = null, name = "5€ Discount", discount = "-5,00€", - discountValue = -500 + discountValue = -500, + isCoupon = true ) ), discountedPrice = "4,99€", @@ -163,7 +173,8 @@ private fun CardWithDiscountItem() { ShoppingCartScreen( uiState = uiState, onItemDeleted = {}, - onQuantityChanged = { _, _ -> } + onQuantityChanged = { _, _ -> }, + onDeleteDiscount = {_,_ -> } ) } @@ -197,7 +208,8 @@ private fun CardWithDeposit() { ShoppingCartScreen( uiState = uiState, onItemDeleted = {}, - onQuantityChanged = { _, _ -> } + onQuantityChanged = { _, _ -> }, + onDeleteDiscount = {_,_ -> } ) } @@ -229,6 +241,7 @@ private fun CardWithCartDiscount() { ShoppingCartScreen( uiState = uiState, onItemDeleted = {}, - onQuantityChanged = { _, _ -> } + onQuantityChanged = { _, _ -> }, + onDeleteDiscount = {_,_ -> } ) } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt index 0af1434292..6f49dd1365 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import io.snabble.sdk.PriceFormatter import io.snabble.sdk.Project import io.snabble.sdk.Snabble +import io.snabble.sdk.extensions.xx import io.snabble.sdk.shoppingcart.ShoppingCart import io.snabble.sdk.shoppingcart.data.item.ItemType import io.snabble.sdk.shoppingcart.data.listener.SimpleShoppingCartListener @@ -82,18 +83,28 @@ class ShoppingCartViewModel : ViewModel() { when (event) { is RemoveItem -> removeItemFromCart(event.item, event.onSuccess) is UpdateQuantity -> updateQuantity(event.item, event.quantity) + is DeleteDiscount -> deleteDiscountFromItem(event.item, event.discountId) } } private fun removeItemFromCart(item: ShoppingCart.Item?, onSuccess: (index: Int) -> Unit) { val index = currentCart?.indexOf(item) ?: return + item?.lineItem?.discountRuleID?.let { + currentCart?.removeCoupon(it) + return + } if (index != -1) { currentCart?.remove(index) + Telemetry.event(Telemetry.Event.DeletedFromCart, item?.product) onSuccess(index) } } + private fun deleteDiscountFromItem(item: ShoppingCart.Item?, discountId: String) { + currentCart?.removeItem(discountId) + } + private fun updateQuantity(item: ShoppingCart.Item, quantity: Int) { item.updateQuantity(quantity) Telemetry.event(Telemetry.Event.CartAmountChanged, item.product) @@ -103,10 +114,22 @@ class ShoppingCartViewModel : ViewModel() { val cartItems: MutableList = mutableListOf() with(cart.filterNotNull()) { filter { it.type == ItemType.PRODUCT }.let { cartItems.addProducts(it) } - filter { it.type == ItemType.DEPOSIT_RETURN_VOUCHER }.let { cartItems.addDepositReturnItems(it) } + filter { it.type == ItemType.DEPOSIT_RETURN_VOUCHER }.let { + cartItems.addDepositReturnItems( + it + ) + } - filter { it.isDiscount && it.lineItem?.discountType != "cart" }.let { cartItems.addDiscountsToProducts(it) } - filter { it.isDiscount && it.lineItem?.discountType == "cart" }.let { cartItems.addCartDiscount(it) } + filter { it.isDiscount && it.lineItem?.discountType != "cart" }.let { + cartItems.addDiscountsToProducts( + it + ) + } + filter { it.isDiscount && it.lineItem?.discountType == "cart" }.let { + cartItems.addCartDiscount( + it + ) + } } cartItems.addPriceModifiersAsDiscountsProducts() @@ -147,7 +170,8 @@ class ShoppingCartViewModel : ViewModel() { val price = item.calculateTotalPrice() // Since the total price can be null as we invalidate the online prices, // we need to use the price text instead to display the product price without an changed instead - val priceText = if (price == 0) item.priceText else priceFormatter?.format(price).orEmpty() + val priceText = + if (price == 0) item.priceText else priceFormatter?.format(price).orEmpty() item.copy( totalPriceText = priceText, @@ -221,6 +245,7 @@ class ShoppingCartViewModel : ViewModel() { modifiedPrice.let { discounts.add( DiscountItem( + id = null, name = name, discount = modifiedPrice, discountValue = discountValue, @@ -246,7 +271,17 @@ class ShoppingCartViewModel : ViewModel() { ?.let { remove(it) val product = it as? ProductItem ?: return@forEach - add(product.copy(discounts = it.discounts + DiscountItem(name, discount, value))) + add( + product.copy( + discounts = it.discounts + DiscountItem( + id = item.id, + name = name, + discount = discount, + discountValue = value, + isCoupon = item.type == ItemType.COUPON + ) + ) + ) } } } @@ -277,6 +312,11 @@ internal data class UpdateQuantity( val quantity: Int ) : Event +internal data class DeleteDiscount( + val item: ShoppingCart.Item, + val discountId: String +) : Event + data class UiState( val items: List = emptyList(), val totalCartPrice: Int? = null diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt index 61722b49dd..f2b3345d9e 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt @@ -2,10 +2,14 @@ package io.snabble.sdk.ui.cart.shoppingcart.cartdiscount import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box 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.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Surface import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -19,6 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import io.snabble.sdk.checkout.LineItem import io.snabble.sdk.shoppingcart.ShoppingCart +import io.snabble.sdk.ui.cart.shoppingcart.CartItem import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.model.CartDiscountItem import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.widget.DiscountDescription @@ -26,24 +31,27 @@ import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.widget.DiscountDescripti internal fun CartDiscount( modifier: Modifier, item: CartDiscountItem, + onDelete: (ShoppingCart.Item) -> Unit ) { - Row( - modifier = modifier.padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Image( - modifier = Modifier.size(44.dp), - painter = painterResource(id = item.imageResId), - contentDescription = stringResource(id = item.title), - contentScale = ContentScale.Fit, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary) - ) - DiscountDescription( - title = stringResource(item.title), - description = item.name, - discount = item.discount + Card( + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary ) + ) { + Row( + modifier = modifier.padding(vertical = 8.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + DiscountDescription( + title = stringResource(item.title), + description = item.name, + discount = item.discount, + onDelete = {onDelete(item.item)} + ) + } } } @@ -57,7 +65,8 @@ private fun Preview() { item = ShoppingCart.Item(ShoppingCart(), LineItem(id = "", amount = 1)), discount = "7.00", name = "SUPER DUPER RABATT" - ) + ), + onDelete = {} ) } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt index 5c7ee3beb0..8c8b38ae27 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt @@ -1,49 +1,48 @@ package io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.widget -import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.text.style.TextAlign import androidx.compose.ui.unit.dp -import io.snabble.sdk.ui.R @Composable -internal fun DiscountDescription(title: String, description: String, discount: String) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { +internal fun DiscountDescription( + title: String, + description: String, + discount: String, + onDelete: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = discount, + style = MaterialTheme.typography.bodyLarge, + ) Text( text = title, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = discount, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - modifier = Modifier.weight(1f), - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.End - ) - Image( - modifier = Modifier.size(18.dp), - painter = painterResource(R.drawable.discount_badge), - contentDescription = "" - ) - } + Text( + modifier = Modifier.weight(1f), + text = description, + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.End + ) + Icon( + Icons.Outlined.DeleteOutline, + modifier = Modifier.clickable(onClick = onDelete), + contentDescription = null + ) } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/DeletableProduct.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/DeletableProduct.kt index 9d9e41e7d1..9f968c5ffa 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/DeletableProduct.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/DeletableProduct.kt @@ -10,7 +10,8 @@ internal fun DeletableProduct( modifier: Modifier, item: ProductItem, onItemDeleted: () -> Unit, - onQuantityChanged: (Int) -> Unit + onQuantityChanged: (Int) -> Unit, + onDeleteDiscount: (String) -> Unit ) { SwipeToDeleteContainer( @@ -20,7 +21,8 @@ internal fun DeletableProduct( Product( cartItem = item, onDeleteItem = onItemDeleted, - onQuantityChanged = onQuantityChanged + onQuantityChanged = onQuantityChanged, + onDeleteDiscount = onDeleteDiscount ) } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/DiscountItem.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/DiscountItem.kt index 690a27d7b1..33f10f6851 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/DiscountItem.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/DiscountItem.kt @@ -1,8 +1,10 @@ package io.snabble.sdk.ui.cart.shoppingcart.product.model internal data class DiscountItem( + val id: String?, val name: String, val discount: String, val discountValue: Int, + val isCoupon: Boolean = false, val useNegativeValue: Boolean = false ) diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt index 999f4a1c01..8182fdd47e 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt @@ -2,19 +2,28 @@ package io.snabble.sdk.ui.cart.shoppingcart.product.widget import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.heightIn 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.outlined.DeleteOutline +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.text.style.TextAlign import androidx.compose.ui.unit.dp import io.snabble.sdk.ui.R import io.snabble.sdk.ui.cart.shoppingcart.product.model.DiscountItem @@ -26,6 +35,7 @@ internal fun Product( cartItem: ProductItem, onQuantityChanged: (Int) -> Unit, onDeleteItem: () -> Unit, + onDeleteDiscount: (String) -> Unit ) { Row( modifier = Modifier @@ -68,17 +78,50 @@ internal fun Product( } } if (cartItem.discounts.isNotEmpty()) { - Discounts(cartItem.discounts) + Discounts(cartItem.discounts, onDeleteDiscount =onDeleteDiscount) } } } } @Composable -private fun Discounts(discounts: List) { +private fun Discounts(discounts: List, onDeleteDiscount: (String) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { discounts.forEach { - DiscountItemWidget(it) + if (it.isCoupon) { + Card( + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Row( + modifier = Modifier.padding(all = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + if (it.useNegativeValue) "-${it.discount}" else it.discount, + style = MaterialTheme.typography.bodySmall, + ) + Text( + it.name, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + ) + Icon( + Icons.Outlined.DeleteOutline, + modifier = Modifier.clickable{ if(it.id != null) onDeleteDiscount(it.id) }, + contentDescription = null + ) + } + } + + } else { + DiscountItemWidget(it) + } } } } From 4b6fa18b3406b961f95390fc029dcfdb32a7ebd7 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 3 Jun 2026 12:18:27 +0200 Subject: [PATCH 2/5] add missing @SerializedName annotation to fix r8 issues and handle missing resume of the scanner after a product is added --- .../snabble/sdk/shoppingcart/ShoppingCart.kt | 13 ++++++++ .../sdk/shoppingcart/ShoppingCartData.kt | 30 ++++++++++--------- .../snabble/sdk/ui/scanner/ProductResolver.kt | 1 + 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt index d095981f3c..f8de3de93f 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt @@ -2,6 +2,7 @@ package io.snabble.sdk.shoppingcart import androidx.annotation.Keep import androidx.annotation.RestrictTo +import com.google.gson.annotations.SerializedName import io.snabble.sdk.PriceFormatter import io.snabble.sdk.Product import io.snabble.sdk.Product.Type @@ -974,16 +975,20 @@ class ShoppingCart( /** * Returns the product associated with the shopping cart item. */ + @SerializedName("product") var product: Product? = null /** * Returns the scanned code which was used when scanning the product and adding it to the shopping cart */ + @SerializedName("scannedCode") var scannedCode: ScannedCode? = null private set + @SerializedName("quantity") var quantity = 0 + @SerializedName("lineItem") var lineItem: LineItem? = null set(value) { field = value @@ -993,9 +998,11 @@ class ShoppingCart( /** * Returns the id of the shopping cart item */ + @SerializedName("id") var id: String? = null private set + @SerializedName("isUsingSpecifiedQuantity") private var isUsingSpecifiedQuantity = false @Transient @@ -1005,24 +1012,29 @@ class ShoppingCart( * Sets or Returns true if a manual coupon (coupon applied by the user after scanning) is applied */ @JvmField + @SerializedName("isManualCouponApplied") var isManualCouponApplied = false /** * Gets the user associated coupon of this item */ + @SerializedName("coupon") var coupon: Coupon? = null /** * Returns the depositReturnVoucher associated with the shopping cart item. */ + @SerializedName("depositReturnVoucher") var depositReturnVoucher: DepositReturnVoucher? = null /** * Returns the deposit associated with the shopping cart item. */ + @SerializedName("deposit") var deposit : Deposit? = null // The local generated UUID of a coupon which which will be used by the backend + @SerializedName("backendCouponId") var backendCouponId: String? = null constructor(cart: ShoppingCart, coupon: Coupon, scannedCode: ScannedCode?) { @@ -1215,6 +1227,7 @@ class ShoppingCart( val totalPrice: Int get() = lineItem?.totalPrice ?: localTotalPrice + @SerializedName("lastPrice") private var lastPrice: Int = 0 /** * Gets the total price of the items, ignoring the backend response diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartData.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartData.kt index 58f11428c4..fac12de7b7 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartData.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartData.kt @@ -1,6 +1,7 @@ package io.snabble.sdk.shoppingcart import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName import io.snabble.sdk.Product import io.snabble.sdk.ViolationNotification import io.snabble.sdk.shoppingcart.data.Taxation @@ -9,20 +10,21 @@ import java.util.UUID @Keep data class ShoppingCartData @JvmOverloads constructor( - val id: String = UUID.randomUUID().toString(), - val uuid: String = UUID.randomUUID().toString(), - val lastModificationTime: Long = 0, - val items: MutableList = mutableListOf(), + @SerializedName("id") val id: String = UUID.randomUUID().toString(), + @SerializedName("uuid") val uuid: String = UUID.randomUUID().toString(), + @SerializedName("lastModificationTime") val lastModificationTime: Long = 0, + @SerializedName("items") val items: MutableList = mutableListOf(), + @SerializedName("violationNotifications") val violationNotifications: MutableList = mutableListOf(), - val modCount: Int = 0, - val addCount: Int = 0, - val onlineTotalPrice: Int? = null, - val invalidProducts: List? = null, - val invalidItemIds: List? = null, - val taxation: Taxation = Taxation.UNDECIDED, - val hasReachedMaxCheckoutLimit: Boolean = false, - val hasReachedMaxOnlinePaymentLimit: Boolean = false, - val backupTimestamp: Long = 0, + @SerializedName("modCount") val modCount: Int = 0, + @SerializedName("addCount") val addCount: Int = 0, + @SerializedName("onlineTotalPrice") val onlineTotalPrice: Int? = null, + @SerializedName("invalidProducts") val invalidProducts: List? = null, + @SerializedName("invalidItemIds") val invalidItemIds: List? = null, + @SerializedName("taxation") val taxation: Taxation = Taxation.UNDECIDED, + @SerializedName("hasReachedMaxCheckoutLimit") val hasReachedMaxCheckoutLimit: Boolean = false, + @SerializedName("hasReachedMaxOnlinePaymentLimit") val hasReachedMaxOnlinePaymentLimit: Boolean = false, + @SerializedName("backupTimestamp") val backupTimestamp: Long = 0, ) { fun applyShoppingCart(shoppingCart: ShoppingCart) { @@ -31,7 +33,7 @@ data class ShoppingCartData @JvmOverloads constructor( } } - fun deepCopy() : ShoppingCartData { + fun deepCopy(): ShoppingCartData { val json = GsonHolder.get().toJson(this) return GsonHolder.get().fromJson(json, ShoppingCartData::class.java) } diff --git a/ui/src/main/java/io/snabble/sdk/ui/scanner/ProductResolver.kt b/ui/src/main/java/io/snabble/sdk/ui/scanner/ProductResolver.kt index 6ae959b3fd..f5c68a5cf7 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/scanner/ProductResolver.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/scanner/ProductResolver.kt @@ -275,6 +275,7 @@ class ProductResolver private constructor(private val context: Context, private project.shoppingCart.add(cartItem) project.shoppingCart.updatePrices(false) checkMinAge(product) + onDismissListener?.onDismiss() } } } From 6728f2b148576d62dbe7e12f82d28ddc0fc70959 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 3 Jun 2026 16:29:44 +0200 Subject: [PATCH 3/5] add support do delete coupons --- .../java/io/snabble/sdk/coupons/Coupon.kt | 14 ++- .../snabble/sdk/shoppingcart/ShoppingCart.kt | 7 +- .../sdk/shoppingcart/ShoppingCartUpdater.kt | 35 +++++- .../cart/shoppingcart/ShoppingCartScreen.kt | 29 +++-- .../shoppingcart/ShoppingCartViewModel.kt | 49 +++++--- .../shoppingcart/cartdiscount/CartDiscount.kt | 17 +-- .../widget/DiscountDescription.kt | 10 -- .../sdk/ui/cart/shoppingcart/coupon/Coupon.kt | 108 ++++++++++++++++++ .../shoppingcart/product/model/CouponItem.kt | 11 ++ .../shoppingcart/product/widget/Product.kt | 89 ++++----------- .../product/widget/ProductImage.kt | 2 + .../sdk/ui/scanner/SelfScanningView.java | 16 +-- 12 files changed, 252 insertions(+), 135 deletions(-) create mode 100644 ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/coupon/Coupon.kt create mode 100644 ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/CouponItem.kt diff --git a/core/src/main/java/io/snabble/sdk/coupons/Coupon.kt b/core/src/main/java/io/snabble/sdk/coupons/Coupon.kt index b702200d04..14c61ed0dc 100644 --- a/core/src/main/java/io/snabble/sdk/coupons/Coupon.kt +++ b/core/src/main/java/io/snabble/sdk/coupons/Coupon.kt @@ -25,6 +25,7 @@ data class Coupon( @SerializedName("disclaimer") val disclaimer: String?, @SerializedName("colors") val colors: Map?, ) : Parcelable { + val isValid: Boolean get() = when (type) { CouponType.DIGITAL -> image != null @@ -40,6 +41,9 @@ data class Coupon( val validUntil: ZonedDateTime? get() = _validUntil?.let { ZonedDateTime.parse(_validUntil) } + @IgnoredOnParcel + var isRedeemed: Boolean = false + @IgnoredOnParcel val backgroundColor get() = parseColor(colors?.get("background"), Color.WHITE) @@ -60,6 +64,7 @@ data class CouponImage( @SerializedName("name") val name: String?, @SerializedName("formats") val formats: List, ) : Parcelable { + val bestResolutionUrl: String get() { val res = Snabble.application.resources @@ -98,7 +103,10 @@ data class CouponImageFormats( ) : Parcelable enum class CouponType { - @SerializedName("manual") MANUAL, - @SerializedName("printed") PRINTED, - @SerializedName("digital") DIGITAL, + @SerializedName("manual") + MANUAL, + @SerializedName("printed") + PRINTED, + @SerializedName("digital") + DIGITAL, } diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt index f8de3de93f..3fad6e74d9 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt @@ -470,7 +470,7 @@ class ShoppingCart( fun removeCoupon(id: String) { val asd = id.replace("_", "-") - val index = indexOfFirst { it?.coupon?.id.xx("cur") == asd.xx("remo")}.xx("index") + val index = indexOfFirst { it?.coupon?.id == asd } if (index != -1) remove(index) } @@ -992,7 +992,7 @@ class ShoppingCart( var lineItem: LineItem? = null set(value) { field = value - value?.let { lastPrice= it.totalPrice } + value?.let { lastPrice = it.totalPrice } } /** @@ -1031,7 +1031,7 @@ class ShoppingCart( * Returns the deposit associated with the shopping cart item. */ @SerializedName("deposit") - var deposit : Deposit? = null + var deposit: Deposit? = null // The local generated UUID of a coupon which which will be used by the backend @SerializedName("backendCouponId") @@ -1229,6 +1229,7 @@ class ShoppingCart( @SerializedName("lastPrice") private var lastPrice: Int = 0 + /** * Gets the total price of the items, ignoring the backend response */ diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt index 7fa9f8a3aa..d65e42bbb4 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCartUpdater.kt @@ -160,7 +160,7 @@ internal class ShoppingCartUpdater( addCartDiscounts(filter { it.type == LineItemType.DISCOUNT && it.discountType == "cart" }) addLineItemsAsCartItems(filter { it.type == LineItemType.DISCOUNT && it.discountType != "cart" }) - addLineItemsAsCartItems(filter { it.type == LineItemType.COUPON }) + addCouponLineItems(filter { it.type == LineItemType.COUPON }) addDepositToItem(filter { it.type == LineItemType.DEPOSIT }) addDepositReturnsToVoucher(filter { it.type == LineItemType.DEPOSIT_RETURN }) } @@ -199,10 +199,17 @@ internal class ShoppingCartUpdater( } private fun addCartDiscounts(cartDiscountItems: List) { - val totalCartDiscount = cartDiscountItems.sumOf { it.totalPrice } - val cartDiscounts = cartDiscountItems.mapNotNull { it.name } - val discount = cartDiscountItems.firstOrNull{ it.discountRuleID != null} - addCartDiscountLineItem(totalCartDiscount, cartDiscounts,discount?.discountRuleID) + val cartLevelDiscounts = cartDiscountItems.filter { it.refersTo == cart.id } + val itemLevelDiscounts = cartDiscountItems.filter { it.refersTo != cart.id } + + if (cartLevelDiscounts.isNotEmpty()) { + val totalCartDiscount = cartLevelDiscounts.sumOf { it.totalPrice } + val cartDiscounts = cartLevelDiscounts.mapNotNull { it.name } + val discount = cartLevelDiscounts.firstOrNull { it.discountRuleID != null } + addCartDiscountLineItem(totalCartDiscount, cartDiscounts, discount?.discountRuleID) + } + + addLineItemsAsCartItems(itemLevelDiscounts) } private fun deserializedCheckoutInfo( @@ -239,6 +246,22 @@ internal class ShoppingCartUpdater( } } + private fun addCouponLineItems(couponLineItems: List) { + couponLineItems.forEach { lineItem -> + val existing = cart.firstOrNull { it?.coupon?.id == lineItem.couponId } + if (existing != null) { + existing.lineItem = lineItem + existing.coupon?.isRedeemed = lineItem.redeemed + } else { + val newItem = cart.newItem(lineItem) + val coupon = project.coupons.get()?.find { it.id == lineItem.couponId } + coupon?.isRedeemed = lineItem.redeemed + newItem.coupon = coupon + cart.insert(newItem, cart.size(), false) + } + } + } + private fun addCartDiscountLineItem(discounts: Int, cartDiscounts: List, discountRuleId: String?) { if (discounts != 0) { val lineItem = LineItem( @@ -246,7 +269,7 @@ internal class ShoppingCartUpdater( amount = 1, discountType = "cart", name = cartDiscounts.firstOrNull(), - discountRuleID =discountRuleId, + discountRuleID = discountRuleId, price = discounts, totalPrice = discounts, type = LineItemType.DISCOUNT diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt index 5c166c717b..bcebb344a0 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartScreen.kt @@ -15,8 +15,10 @@ import io.snabble.sdk.checkout.LineItem import io.snabble.sdk.shoppingcart.ShoppingCart import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.CartDiscount import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.model.CartDiscountItem +import io.snabble.sdk.ui.cart.shoppingcart.coupon.Coupon import io.snabble.sdk.ui.cart.shoppingcart.depositreturn.DepositReturn import io.snabble.sdk.ui.cart.shoppingcart.product.DeletableProduct +import io.snabble.sdk.ui.cart.shoppingcart.product.model.CouponItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.DepositItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.DepositReturnItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.DiscountItem @@ -47,8 +49,11 @@ fun ShoppingCartScreen( onQuantityChanged = { item, quantity -> viewModel.onEvent(UpdateQuantity(item, quantity)) }, - onDeleteDiscount = {item, discountId -> + onDeleteDiscount = { item, discountId -> viewModel.onEvent(DeleteDiscount(item, discountId)) + }, + onCouponDeleted = { couponId -> + viewModel.onEvent(DeleteCoupon(couponId)) } ) } @@ -59,12 +64,13 @@ private fun ShoppingCartScreen( uiState: UiState, modifier: Modifier = Modifier, onItemDeleted: (ShoppingCart.Item) -> Unit, + onCouponDeleted: (String) -> Unit, onQuantityChanged: (ShoppingCart.Item, Int) -> Unit, onDeleteDiscount: (ShoppingCart.Item, String) -> Unit, ) { LazyColumn( - modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection()) + modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection()), ) { itemsIndexed(items = uiState.items, key = { _, item -> item.hashCode() }) { index, cartItem -> when (cartItem) { @@ -87,15 +93,16 @@ private fun ShoppingCartScreen( DepositReturn(item = cartItem, showHint = showHint, onDeleteClick = onItemDeleted) } + is CouponItem -> Coupon(couponItem = cartItem, onDelete = onCouponDeleted) + is CartDiscountItem -> { CartDiscount( modifier = Modifier.fillMaxWidth(), item = cartItem, - onDelete = onItemDeleted ) } } - if (index != uiState.items.lastIndex) HorizontalDivider() + if (index < uiState.items.lastIndex) HorizontalDivider() } } } @@ -136,8 +143,8 @@ private fun CardWithDefaultItems() { uiState = uiState, onItemDeleted = {}, onQuantityChanged = { _, _ -> }, - onDeleteDiscount = {_,_ ->} - ) + onDeleteDiscount = { _, _ -> }, + onCouponDeleted = {}) } @Preview @@ -158,7 +165,6 @@ private fun CardWithDiscountItem() { name = "5€ Discount", discount = "-5,00€", discountValue = -500, - isCoupon = true ) ), discountedPrice = "4,99€", @@ -174,7 +180,8 @@ private fun CardWithDiscountItem() { uiState = uiState, onItemDeleted = {}, onQuantityChanged = { _, _ -> }, - onDeleteDiscount = {_,_ -> } + onDeleteDiscount = { _, _ -> }, + onCouponDeleted = {} ) } @@ -209,7 +216,8 @@ private fun CardWithDeposit() { uiState = uiState, onItemDeleted = {}, onQuantityChanged = { _, _ -> }, - onDeleteDiscount = {_,_ -> } + onDeleteDiscount = { _, _ -> }, + onCouponDeleted = {} ) } @@ -242,6 +250,7 @@ private fun CardWithCartDiscount() { uiState = uiState, onItemDeleted = {}, onQuantityChanged = { _, _ -> }, - onDeleteDiscount = {_,_ -> } + onDeleteDiscount = { _, _ -> }, + onCouponDeleted = {} ) } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt index 6f49dd1365..509150c9ea 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt @@ -6,11 +6,11 @@ import androidx.lifecycle.viewModelScope import io.snabble.sdk.PriceFormatter import io.snabble.sdk.Project import io.snabble.sdk.Snabble -import io.snabble.sdk.extensions.xx import io.snabble.sdk.shoppingcart.ShoppingCart import io.snabble.sdk.shoppingcart.data.item.ItemType import io.snabble.sdk.shoppingcart.data.listener.SimpleShoppingCartListener import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.model.CartDiscountItem +import io.snabble.sdk.ui.cart.shoppingcart.product.model.CouponItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.DepositItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.DepositReturnItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.DiscountItem @@ -84,6 +84,7 @@ class ShoppingCartViewModel : ViewModel() { is RemoveItem -> removeItemFromCart(event.item, event.onSuccess) is UpdateQuantity -> updateQuantity(event.item, event.quantity) is DeleteDiscount -> deleteDiscountFromItem(event.item, event.discountId) + is DeleteCoupon -> deleteCoupon(event.id) } } @@ -101,6 +102,10 @@ class ShoppingCartViewModel : ViewModel() { } } + private fun deleteCoupon(id: String) { + currentCart?.removeCoupon(id) + } + private fun deleteDiscountFromItem(item: ShoppingCart.Item?, discountId: String) { currentCart?.removeItem(discountId) } @@ -114,22 +119,20 @@ class ShoppingCartViewModel : ViewModel() { val cartItems: MutableList = mutableListOf() with(cart.filterNotNull()) { filter { it.type == ItemType.PRODUCT }.let { cartItems.addProducts(it) } - filter { it.type == ItemType.DEPOSIT_RETURN_VOUCHER }.let { - cartItems.addDepositReturnItems( - it - ) - } + filter { it.type == ItemType.DEPOSIT_RETURN_VOUCHER }.let { cartItems.addDepositReturnItems(it) } + filter { it.type == ItemType.COUPON }.let { cartItems.addCouponItems(it) } - filter { it.isDiscount && it.lineItem?.discountType != "cart" }.let { - cartItems.addDiscountsToProducts( - it - ) + filter { + it.isDiscount && (it.lineItem?.discountType != "cart" || it.lineItem?.refersTo != currentCart?.id) + }.let { + cartItems.addDiscountsToProducts(it) } - filter { it.isDiscount && it.lineItem?.discountType == "cart" }.let { - cartItems.addCartDiscount( - it - ) + filter { + it.isDiscount && it.lineItem?.discountType == "cart" && it.lineItem?.refersTo == currentCart?.id + }.let { + cartItems.addCartDiscount(it) } + } cartItems.addPriceModifiersAsDiscountsProducts() @@ -139,6 +142,19 @@ class ShoppingCartViewModel : ViewModel() { _uiState.update { it.copy(items = cartItems, totalCartPrice = currentCart?.totalPrice) } } + private fun MutableList.addCouponItems(items: List) { + items.forEach { item -> + add( + CouponItem( + item = item, + couponId = item.lineItem?.couponId ?: item.coupon?.id, + name = item.lineItem?.name ?: item.coupon?.name, + areRequirementsMet = item.lineItem?.redeemed ?: item.coupon?.isRedeemed ?: false + ) + ) + } + } + private fun MutableList.addDepositReturnItems(items: List) { items.forEach { item -> val totalDepositReturnPrice = @@ -278,7 +294,6 @@ class ShoppingCartViewModel : ViewModel() { name = name, discount = discount, discountValue = value, - isCoupon = item.type == ItemType.COUPON ) ) ) @@ -317,6 +332,10 @@ internal data class DeleteDiscount( val discountId: String ) : Event +internal data class DeleteCoupon( + val id: String +) : Event + data class UiState( val items: List = emptyList(), val totalCartPrice: Int? = null diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt index f2b3345d9e..7299c68287 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/CartDiscount.kt @@ -1,29 +1,22 @@ package io.snabble.sdk.ui.cart.shoppingcart.cartdiscount -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box 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.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Surface import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import io.snabble.sdk.checkout.LineItem import io.snabble.sdk.shoppingcart.ShoppingCart -import io.snabble.sdk.ui.cart.shoppingcart.CartItem import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.model.CartDiscountItem import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.widget.DiscountDescription @@ -31,17 +24,17 @@ import io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.widget.DiscountDescripti internal fun CartDiscount( modifier: Modifier, item: CartDiscountItem, - onDelete: (ShoppingCart.Item) -> Unit ) { Card( + modifier = Modifier.padding(16.dp), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primary, + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), contentColor = MaterialTheme.colorScheme.onPrimary ) ) { Row( - modifier = modifier.padding(vertical = 8.dp, horizontal = 16.dp), + modifier = modifier.padding(vertical = 12.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -49,7 +42,6 @@ internal fun CartDiscount( title = stringResource(item.title), description = item.name, discount = item.discount, - onDelete = {onDelete(item.item)} ) } } @@ -66,7 +58,6 @@ private fun Preview() { discount = "7.00", name = "SUPER DUPER RABATT" ), - onDelete = {} ) } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt index 8c8b38ae27..702ab152cd 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/cartdiscount/widget/DiscountDescription.kt @@ -1,11 +1,7 @@ package io.snabble.sdk.ui.cart.shoppingcart.cartdiscount.widget -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,7 +15,6 @@ internal fun DiscountDescription( title: String, description: String, discount: String, - onDelete: () -> Unit ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -39,10 +34,5 @@ internal fun DiscountDescription( style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.End ) - Icon( - Icons.Outlined.DeleteOutline, - modifier = Modifier.clickable(onClick = onDelete), - contentDescription = null - ) } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/coupon/Coupon.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/coupon/Coupon.kt new file mode 100644 index 0000000000..a8cd71a473 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/coupon/Coupon.kt @@ -0,0 +1,108 @@ +package io.snabble.sdk.ui.cart.shoppingcart.coupon + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.outlined.DeleteOutline +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.snabble.sdk.checkout.LineItem +import io.snabble.sdk.shoppingcart.ShoppingCart +import io.snabble.sdk.ui.R +import io.snabble.sdk.ui.cart.shoppingcart.product.model.CouponItem + +@Composable +internal fun Coupon(couponItem: CouponItem, onDelete: (String) -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primary.copy(if (couponItem.areRequirementsMet) 1f else 0.5f), + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 16.dp), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + couponItem.name?.let { + Text( + it, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + } + if (!couponItem.areRequirementsMet) { + Text( + text = stringResource(id = R.string.Snabble_Coupon_notUsable), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center + ) + } + } + Icon( + Icons.Outlined.DeleteOutline, + modifier = Modifier + .align(Alignment.CenterEnd) + .size(28.dp) + .clickable { if (couponItem.couponId != null) onDelete(couponItem.couponId) }, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + Coupon( + couponItem = CouponItem( + item = ShoppingCart.Item(ShoppingCart(), LineItem(id = "", amount = 1)), + name = "TEST COUPON", + areRequirementsMet = true, + couponId = "123" + ) + ) {} +} + +@Preview +@Composable +private fun PreviewRequirementMissing() { + Coupon( + couponItem = CouponItem( + item = ShoppingCart.Item(ShoppingCart(), LineItem(id = "", amount = 1)), + name = "TEST COUPON", + areRequirementsMet = false, + couponId = "123" + ) + ) {} +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/CouponItem.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/CouponItem.kt new file mode 100644 index 0000000000..36645a81a5 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/model/CouponItem.kt @@ -0,0 +1,11 @@ +package io.snabble.sdk.ui.cart.shoppingcart.product.model + +import io.snabble.sdk.shoppingcart.ShoppingCart +import io.snabble.sdk.ui.cart.shoppingcart.CartItem + +internal data class CouponItem ( + override val item: ShoppingCart.Item, + val couponId: String?, + val name: String? = null, + val areRequirementsMet: Boolean = false, +) : CartItem diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt index 8182fdd47e..ada7580fdf 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/Product.kt @@ -1,31 +1,21 @@ package io.snabble.sdk.ui.cart.shoppingcart.product.widget -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.heightIn 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.outlined.DeleteOutline import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.text.style.TextAlign import androidx.compose.ui.unit.dp -import io.snabble.sdk.ui.R import io.snabble.sdk.ui.cart.shoppingcart.product.model.DiscountItem import io.snabble.sdk.ui.cart.shoppingcart.product.model.ProductItem import io.snabble.sdk.ui.cart.shoppingcart.product.widget.description.ProductDescription @@ -78,7 +68,7 @@ internal fun Product( } } if (cartItem.discounts.isNotEmpty()) { - Discounts(cartItem.discounts, onDeleteDiscount =onDeleteDiscount) + Discounts(cartItem.discounts, onDeleteDiscount = onDeleteDiscount) } } } @@ -88,65 +78,30 @@ internal fun Product( private fun Discounts(discounts: List, onDeleteDiscount: (String) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { discounts.forEach { - if (it.isCoupon) { - Card( - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) + Card( + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Row( + modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier.padding(all = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - if (it.useNegativeValue) "-${it.discount}" else it.discount, - style = MaterialTheme.typography.bodySmall, - ) - Text( - it.name, - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodySmall, - ) - Icon( - Icons.Outlined.DeleteOutline, - modifier = Modifier.clickable{ if(it.id != null) onDeleteDiscount(it.id) }, - contentDescription = null - ) - } + Text( + if (it.useNegativeValue) "-${it.discount}" else it.discount, + style = MaterialTheme.typography.bodySmall, + ) + Text( + it.name, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.End, + ) } - - } else { - DiscountItemWidget(it) } } } } - -@Composable -private fun DiscountItemWidget(it: DiscountItem) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - if (it.useNegativeValue) "-${it.discount}" else it.discount, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(Modifier.weight(1f)) - Text( - it.name, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - Image( - modifier = Modifier.size(18.dp), - painter = painterResource(R.drawable.discount_badge), - contentDescription = "" - ) - } -} diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/ProductImage.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/ProductImage.kt index 62e3e227d5..946fe2775c 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/ProductImage.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/product/widget/ProductImage.kt @@ -29,6 +29,8 @@ internal fun ProductImage( isAgeRestricted: Boolean, age: Int ) { + if (imageUrl == null && !showPlaceholder && !(isAgeRestricted && age > 0)) return + Box(modifier = Modifier.wrapContentSize()) { if (imageUrl != null) { var hasFailed by remember(imageUrl) { mutableStateOf(false) } diff --git a/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java b/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java index 390fabe7b9..3c23fdc173 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java +++ b/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java @@ -225,18 +225,18 @@ public void lookupAndShowProduct(List scannedCodes, BarcodeFormat b .setOnProductNotFoundListener(() -> { final Boolean couponAdded = handleCoupon(scannedCodes); if (!couponAdded) { - handleReturnDepositVoucher(scannedCodes); - } else { - showWarning(getResources().getString(I18nUtils.getIdentifier(getResources(), R.string.Snabble_Scanner_unknownBarcode))); + final boolean voucherAdded = handleReturnDepositVoucher(scannedCodes); + if (!voucherAdded) + showWarning(getResources().getString(I18nUtils.getIdentifier(getResources(), R.string.Snabble_Scanner_unknownBarcode))); } }) .setOnNetworkErrorListener(() -> { final Boolean couponAdded = handleCoupon(scannedCodes); if (!couponAdded) { - handleReturnDepositVoucher(scannedCodes); - } else { - showWarning(getResources().getString(R.string.Snabble_Scanner_networkError)); + final boolean voucherAdded = handleReturnDepositVoucher(scannedCodes); + if (!voucherAdded) + showWarning(getResources().getString(I18nUtils.getIdentifier(getResources(), R.string.Snabble_Scanner_unknownBarcode))); } }) .setOnShelfCodeScannedListener(() -> @@ -267,13 +267,13 @@ public void lookupAndShowProduct(List scannedCodes, BarcodeFormat b .resolve(); } - private void handleReturnDepositVoucher(List scannedCodes) { + private boolean handleReturnDepositVoucher(List scannedCodes) { final kotlin.Pair codeTemplateScannedCodePair = DepositReturnVoucherHelper.getDrvCodeTemplateWithScannedCode(project, scannedCodes); if (codeTemplateScannedCodePair != null) { DepositReturnVoucherHelper.insertDepositReturnVoucherItem(shoppingCart, codeTemplateScannedCodePair.getFirst(), codeTemplateScannedCodePair.getSecond()); showInfo(getResources().getString(R.string.Snabble_Scanner_DepositReturnVoucher_Added)); } - showWarning(getResources().getString(I18nUtils.getIdentifier(getResources(), R.string.Snabble_Scanner_unknownBarcode))); + return codeTemplateScannedCodePair != null; } private Boolean handleCoupon(List scannedCodes) { From d8f86a76a492e846133cade1ce9790b7178e40bf Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 3 Jun 2026 16:35:47 +0200 Subject: [PATCH 4/5] fix last issues --- .../snabble/sdk/shoppingcart/ShoppingCart.kt | 3 +-- .../shoppingcart/ShoppingCartViewModel.kt | 27 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt index 3fad6e74d9..fcbfa62dc2 100644 --- a/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt +++ b/core/src/main/java/io/snabble/sdk/shoppingcart/ShoppingCart.kt @@ -469,8 +469,7 @@ class ShoppingCart( } fun removeCoupon(id: String) { - val asd = id.replace("_", "-") - val index = indexOfFirst { it?.coupon?.id == asd } + val index = indexOfFirst { it?.coupon?.id == id } if (index != -1) remove(index) } diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt index 509150c9ea..b9afc7a283 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/ShoppingCartViewModel.kt @@ -283,21 +283,20 @@ class ShoppingCartViewModel : ViewModel() { val name = item.displayName.orEmpty() val value = item.totalPrice - firstOrNull { it.item.id == item.lineItem?.refersTo } - ?.let { - remove(it) - val product = it as? ProductItem ?: return@forEach - add( - product.copy( - discounts = it.discounts + DiscountItem( - id = item.id, - name = name, - discount = discount, - discountValue = value, - ) - ) + val index = indexOfFirst { it.item.id == item.lineItem?.refersTo } + if (index == -1) return@forEach + val product = getOrNull(index) as? ProductItem ?: return@forEach + set( + index, + product.copy( + discounts = product.discounts + DiscountItem( + id = item.id, + name = name, + discount = discount, + discountValue = value, ) - } + ) + ) } } From 78a70195432c7a3126098e77316faa71425bbd15 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 3 Jun 2026 16:37:29 +0200 Subject: [PATCH 5/5] update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 905500e562..2ea6792500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file. ### Removed ### Fixed +## [0.83.3] +### Changed +* ui: show coupons as part of the shopping cart and change discount design + ## [0.83.2] ### Changed * ui: update unknown barcode message