Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-08 10:40:44 +02:00
parent 8654ee6943
commit 02ac739980
9 changed files with 549 additions and 3 deletions

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems
import com.tangem.features.tangempay.ui.tangemPayTxHistoryItemsV2
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import kotlinx.coroutines.flow.StateFlow
@ -20,7 +21,11 @@ internal class DefaultTangemPayTxHistoryComponent(
override val state: StateFlow<TangemPayTxHistoryUM> = model.uiState
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TangemPayTxHistoryUM) {
tangemPayTxHistoryItems(listState, state)
if (model.isRedesignEnabled()) {
tangemPayTxHistoryItemsV2(listState, state)
} else {
tangemPayTxHistoryItems(listState, state)
}
}
data class Params(

View file

@ -2,6 +2,7 @@ package com.tangem.features.tangempay.components.txHistory
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.ImageReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.themedColor
@ -37,10 +38,12 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
onClick = {},
amount = "-4.99 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "16:41",
title = stringReference("StarbucksStarbucksStarbucksStarbucks"),
subtitle = stringReference("Food&Drinks"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -48,11 +51,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "12:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_arrow_up_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -60,11 +65,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "+126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.accent },
amountColorV2 = { TangemTheme.colors.text.accent },
time = "12:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_arrow_down_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -72,11 +79,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "12:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle(title = "Yesterday", itemKey = "Yesterday"),
@ -85,11 +94,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-4.99 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "21:41",
onClick = {},
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -97,11 +108,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "10:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -109,11 +122,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-4.99 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "19:41",
onClick = {},
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -121,11 +136,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "18:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -133,11 +150,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-4.99 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "17:41",
onClick = {},
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -145,11 +164,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "16:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -157,11 +178,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-4.99 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "15:41",
onClick = {},
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
@ -169,11 +192,13 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor
id = "signiferumque",
amount = "-126.20 USD",
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors.text.primary1 },
time = "14:04",
onClick = {},
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
icon = ImageReference.Res(R.drawable.ic_category_24),
iconV2 = TangemIconUM.Icon(R.drawable.ic_category_24),
),
),
),

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
@ -26,4 +27,19 @@ internal sealed class TangemPayEmptyTransactionHistoryState {
override val iconRes: Int = R.drawable.ic_empty_token_64
override val text: TextReference = resourceReference(R.string.transaction_history_empty_transactions)
}
}
@Immutable
internal sealed interface TangemPayEmptyTransactionHistoryStateV2 {
val text: TextReference
data class FailedToLoad(
val onReload: () -> Unit,
override val text: TextReference = resourceReference(R.string.transaction_history_error_failed_to_load),
) : TangemPayEmptyTransactionHistoryStateV2
data object Empty : TangemPayEmptyTransactionHistoryStateV2 {
override val text: TextReference = resourceReference(R.string.transaction_history_empty_transactions)
}
}

View file

@ -1,9 +1,13 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.ColorReference
import com.tangem.core.ui.extensions.ColorReference2
import com.tangem.core.ui.extensions.ImageReference
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed interface TangemPayTransactionState {
val id: String
@ -15,9 +19,11 @@ internal sealed interface TangemPayTransactionState {
val onClick: () -> Unit
val amount: String
val amountColor: ColorReference
val amountColorV2: ColorReference2
val title: TextReference
val subtitle: TextReference
val icon: ImageReference
val iconV2: TangemIconUM
val time: String
data class Spend(
@ -25,9 +31,11 @@ internal sealed interface TangemPayTransactionState {
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: ColorReference,
override val amountColorV2: ColorReference2,
override val title: TextReference,
override val subtitle: TextReference,
override val icon: ImageReference,
override val iconV2: TangemIconUM,
override val time: String,
) : Content
@ -36,9 +44,11 @@ internal sealed interface TangemPayTransactionState {
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: ColorReference,
override val amountColorV2: ColorReference2,
override val title: TextReference,
override val subtitle: TextReference,
override val icon: ImageReference,
override val iconV2: TangemIconUM,
override val time: String,
) : Content
@ -47,9 +57,11 @@ internal sealed interface TangemPayTransactionState {
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: ColorReference,
override val amountColorV2: ColorReference2,
override val title: TextReference,
override val subtitle: TextReference,
override val icon: ImageReference,
override val iconV2: TangemIconUM,
override val time: String,
) : Content
@ -58,9 +70,11 @@ internal sealed interface TangemPayTransactionState {
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: ColorReference,
override val amountColorV2: ColorReference2,
override val title: TextReference,
override val subtitle: TextReference,
override val icon: ImageReference,
override val iconV2: TangemIconUM,
override val time: String,
) : Content
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
internal sealed interface TangemPayTxHistoryUM {
val isBalanceHidden: Boolean
@ -11,6 +13,11 @@ internal sealed interface TangemPayTxHistoryUM {
data class Loading(override val isBalanceHidden: Boolean) : TangemPayTxHistoryUM {
val items = persistentListOf(
TangemPayTxHistoryItemUM.Title,
TangemPayTxHistoryItemUM.GroupTitle(
title = GROUP_TITLE_LOADING_PLACEHOLDER,
itemKey = GROUP_TITLE_LOADING_ITEM_KEY,
isLoading = true,
),
TangemPayTxHistoryItemUM.Transaction(TangemPayTransactionState.Loading("LOADING_TX_HASH_1")),
TangemPayTxHistoryItemUM.Transaction(TangemPayTransactionState.Loading("LOADING_TX_HASH_2")),
TangemPayTxHistoryItemUM.Transaction(TangemPayTransactionState.Loading("LOADING_TX_HASH_3")),
@ -35,11 +42,22 @@ internal sealed interface TangemPayTxHistoryUM {
}
}
@Immutable
sealed interface TangemPayTxHistoryItemUM {
data object Title : TangemPayTxHistoryItemUM
data class GroupTitle(val title: String, val itemKey: String) : TangemPayTxHistoryItemUM {
data class GroupTitle(
val title: String,
val itemKey: String,
val isLoading: Boolean = false,
) : TangemPayTxHistoryItemUM {
val legacyGroupTitle = TxHistoryState.TxHistoryItemState.GroupTitle(title = title, itemKey = itemKey)
}
data class Transaction(val transaction: TangemPayTransactionState) : TangemPayTxHistoryItemUM
}
companion object {
/** Placeholder copy for [TextShimmer] height; real date labels vary in width but not in line metrics. */
const val GROUP_TITLE_LOADING_PLACEHOLDER: String = "Today"
const val GROUP_TITLE_LOADING_ITEM_KEY: String = "loading-group-title"
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.utils.TangemPayTxHistoryListManager
@ -25,6 +26,7 @@ internal class TangemPayTxHistoryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val featureToggles: TangemPayFeatureToggles,
) : Model() {
private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require()
@ -44,6 +46,8 @@ internal class TangemPayTxHistoryModel @Inject constructor(
subscribeToUpdateListener()
}
fun isRedesignEnabled(): Boolean = featureToggles.isRedesignEnabled
private fun launchPagination() {
modelScope.launch { listManager.launchPagination(params.userWalletId) }
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.ImageReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -7,6 +8,9 @@ import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_24
import com.tangem.core.ui.res.generated.icons.ic_arrow_up_24
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.details.impl.R
@ -33,6 +37,7 @@ internal class TangemPayTxHistoryItemsConverter(
}
}
@Suppress("CyclomaticComplexMethod")
private fun convertSpend(spend: TangemPayTxHistoryItem.Spend): TangemPayTransactionState.Content.Spend {
val localDate = spend.date.withZone(DateTimeZone.getDefault())
val amountPrefix = when {
@ -59,11 +64,29 @@ internal class TangemPayTxHistoryItemsConverter(
else -> TangemTheme.colors.text.primary1
}
},
amountColorV2 = {
if (spend.status == TangemPayTxHistoryItem.Status.DECLINED) {
TangemTheme.colors3.text.status.error
} else {
TangemTheme.colors3.text.primary
}
},
title = stringReference(spend.enrichedMerchantName ?: spend.merchantName),
subtitle = paySpendSubtitleConverter.convert(spend),
time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter),
icon = spend.enrichedMerchantIconUrl?.let(ImageReference::Url)
?: ImageReference.Res(R.drawable.ic_category_24),
iconV2 = if (spend.enrichedMerchantIconUrl != null) {
TangemIconUM.Url(
url = spend.enrichedMerchantIconUrl,
fallbackRes = R.drawable.ic_category_24,
)
} else {
TangemIconUM.Icon(
iconRes = R.drawable.ic_category_24,
tintReference = { TangemTheme.colors3.icon.secondary },
)
},
)
}
@ -76,10 +99,15 @@ internal class TangemPayTxHistoryItemsConverter(
onClick = { txHistoryUiActions.onTransactionClick(payment) },
amount = amount,
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors3.text.primary },
title = resourceReference(R.string.tangem_pay_withdrawal),
subtitle = stringReference("Transfers"),
time = DateTimeFormatters.formatDate(payment.date, DateTimeFormatters.timeFormatter),
icon = ImageReference.Res(R.drawable.ic_arrow_up_24),
iconV2 = TangemIconUM.Icon(
imageVector = Icons.ic_arrow_up_24,
tintReference = { TangemTheme.colors3.icon.secondary },
),
)
}
@ -93,10 +121,16 @@ internal class TangemPayTxHistoryItemsConverter(
onClick = { txHistoryUiActions.onTransactionClick(fee) },
amount = amount,
amountColor = themedColor { TangemTheme.colors.text.primary1 },
amountColorV2 = { TangemTheme.colors3.text.primary },
title = resourceReference(R.string.tangem_pay_fee_title),
subtitle = fee.description?.let(::stringReference) ?: resourceReference(R.string.tangem_pay_fee_subtitle),
icon = ImageReference.Res(R.drawable.ic_percent_24),
time = DateTimeFormatters.formatDate(fee.date, DateTimeFormatters.timeFormatter),
iconV2 = TangemIconUM.Icon(
iconRes = R.drawable.ic_percent_24,
tintReference = { TangemTheme.colors3.icon.secondary },
),
)
}
@ -119,6 +153,7 @@ internal class TangemPayTxHistoryItemsConverter(
TangemPayTxHistoryItem.Type.Deposit -> themedColor { TangemTheme.colors.text.accent }
TangemPayTxHistoryItem.Type.Withdrawal -> themedColor { TangemTheme.colors.text.primary1 }
},
amountColorV2 = { TangemTheme.colors3.text.primary },
title = when (collateral.type) {
TangemPayTxHistoryItem.Type.Deposit -> resourceReference(R.string.tangem_pay_deposit)
TangemPayTxHistoryItem.Type.Withdrawal -> resourceReference(R.string.tangem_pay_withdrawal)
@ -129,6 +164,13 @@ internal class TangemPayTxHistoryItemsConverter(
TangemPayTxHistoryItem.Type.Withdrawal -> ImageReference.Res(R.drawable.ic_arrow_up_24)
},
time = DateTimeFormatters.formatDate(collateral.date, DateTimeFormatters.timeFormatter),
iconV2 = TangemIconUM.Icon(
imageVector = when (collateral.type) {
TangemPayTxHistoryItem.Type.Deposit -> Icons.ic_arrow_down_24
TangemPayTxHistoryItem.Type.Withdrawal -> Icons.ic_arrow_up_24
},
tintReference = { TangemTheme.colors3.icon.secondary },
),
)
}
}

View file

@ -128,7 +128,19 @@ private fun TangemPayTxHistoryListItem(
) {
when (state) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> {
TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier)
if (state.isLoading) {
RectangleShimmer(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
)
.height(TangemTheme.dimens.size12),
)
} else {
TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier)
}
}
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> {
TangemPayTxHistoryTitle(modifier = modifier)

View file

@ -0,0 +1,410 @@
package com.tangem.features.tangempay.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowText
import com.tangem.core.ui.ds2.row.TangemRowTextRole
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.ds2.shimmers.RectangleShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_refresh_20
import com.tangem.core.ui.res.generated.icons.ic_binoculars_20
import com.tangem.core.ui.test.EmptyTransactionBlockTestTags
import com.tangem.features.tangempay.entity.TangemPayEmptyTransactionHistoryStateV2
import com.tangem.features.tangempay.entity.TangemPayTransactionState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
private const val LOAD_ITEMS_BUFFER = 20
internal fun LazyListScope.tangemPayTxHistoryItemsV2(listState: LazyListState, state: TangemPayTxHistoryUM) {
when (state) {
is TangemPayTxHistoryUM.Content -> {
contentItems(listState = listState, state = state)
}
is TangemPayTxHistoryUM.Empty -> {
nonContentItem(listState = listState, state = TangemPayEmptyTransactionHistoryStateV2.Empty)
}
is TangemPayTxHistoryUM.Error -> {
nonContentItem(
listState = listState,
state = TangemPayEmptyTransactionHistoryStateV2.FailedToLoad(onReload = state.onReload),
)
}
is TangemPayTxHistoryUM.Loading -> {
loadingItems(state = state)
}
}
}
private fun LazyListScope.nonContentItem(
listState: LazyListState,
state: TangemPayEmptyTransactionHistoryStateV2,
modifier: Modifier = Modifier,
) {
val itemKey = state::class.java
item(key = itemKey, contentType = itemKey) {
val fillRemaining = Modifier.heightIn(min = rememberRemainingViewportHeight(listState, itemKey))
when (state) {
is TangemPayEmptyTransactionHistoryStateV2.Empty -> {
TangemPayTransactionEmptyBlock(
state = state,
modifier = modifier
.then(fillRemaining)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3)
.fillMaxWidth(),
)
}
is TangemPayEmptyTransactionHistoryStateV2.FailedToLoad -> {
TangemPayFailedTransactionBlock(
state = state,
modifier = modifier
.then(fillRemaining)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3)
.fillMaxWidth(),
)
}
}
}
}
private fun LazyListScope.contentItems(listState: LazyListState, state: TangemPayTxHistoryUM.Content) {
itemsIndexed(
items = state.items,
key = { index, item ->
when (item) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> item.itemKey
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> index + item.hashCode()
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction ->
item.transaction.id + (item.transaction as? TangemPayTransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { _, item ->
TangemPayTxHistoryListItem(
state = item,
isBalanceHidden = state.isBalanceHidden,
)
},
)
item {
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = state.loadMore,
)
}
}
private fun LazyListScope.loadingItems(state: TangemPayTxHistoryUM.Loading) {
itemsIndexed(
items = state.items,
key = { _, item ->
when (item) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> item.itemKey
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> item.hashCode()
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction ->
item.transaction.id + (item.transaction as? TangemPayTransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { _, item ->
TangemPayTxHistoryListItem(
state = item,
isBalanceHidden = true,
)
},
)
}
@Composable
private fun TangemPayTxHistoryListItem(
state: TangemPayTxHistoryUM.TangemPayTxHistoryItemUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> GroupTitleBlock(state, modifier)
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> Unit
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction -> {
TangemPayTransaction(
transactionState = state.transaction,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
}
}
@Composable
private fun GroupTitleBlock(
state: TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle,
modifier: Modifier = Modifier,
) {
if (state.isLoading) {
TextShimmer(
modifier = modifier.width(TangemTheme.dimens2.x10),
text = state.title,
style = TextShimmerStyle.SUBHEADING,
radius = TangemTheme.dimens2.x25,
)
} else {
Text(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = TangemTheme.dimens2.x3,
horizontal = TangemTheme.dimens2.x4,
),
text = state.legacyGroupTitle.title,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
@Composable
private fun TangemPayTransaction(
transactionState: TangemPayTransactionState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
TangemRow(
verticalAlignment = TangemRowVerticalAlignment.Center,
modifier = modifier.clickable(
enabled = transactionState is TangemPayTransactionState.Content,
onClick = (transactionState as? TangemPayTransactionState.Content)?.onClick ?: {},
),
startSlot = { Icon(state = transactionState, modifier = Modifier.size(TangemTheme.dimens2.x10)) },
titleSlot = { Title(state = transactionState) },
subtitleSlot = { Subtitle(state = transactionState) },
valueSlot = { Amount(state = transactionState, isBalanceHidden = isBalanceHidden) },
subvalueSlot = { Timestamp(state = transactionState) },
)
}
@Composable
private fun Icon(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> TransactionListIcon(
iconState = state.iconV2,
modifier = modifier,
)
is TangemPayTransactionState.Loading -> RectangleShimmer(
modifier = modifier.size(TangemTheme.dimens2.x10),
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun TransactionListIcon(iconState: TangemIconUM, modifier: Modifier = Modifier) {
when (iconState) {
is TangemIconUM.Url -> {
TangemIcon(
tangemIconUM = iconState,
modifier = modifier
.size(TangemTheme.dimens2.x10)
.clip(CircleShape),
)
}
else -> {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x10)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.opaque.primary),
contentAlignment = Alignment.Center,
) {
TangemIcon(
tangemIconUM = iconState,
modifier = Modifier.size(TangemTheme.dimens.size20),
)
}
}
}
}
@Composable
private fun Title(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
TangemRowText(text = state.title.resolveReference(), role = TangemRowTextRole.Title)
}
is TangemPayTransactionState.Loading -> {
TextShimmer(
modifier = modifier,
text = "Transfer",
radius = TangemTheme.dimens2.x25,
style = TextShimmerStyle.BODY,
)
}
}
}
@Composable
private fun Subtitle(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
TangemRowText(text = state.subtitle.resolveReference(), role = TangemRowTextRole.Subtitle)
}
is TangemPayTransactionState.Loading -> {
TextShimmer(
modifier = modifier,
text = "Transfer",
radius = TangemTheme.dimens2.x25,
style = TextShimmerStyle.CAPTION,
)
}
}
}
@Composable
private fun Amount(state: TangemPayTransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
Text(
text = state.amount.orMaskWithStars(isBalanceHidden),
modifier = modifier,
textAlign = TextAlign.End,
color = state.amountColorV2(),
style = TangemTheme.typography3.body.medium,
)
}
is TangemPayTransactionState.Loading -> {
TextShimmer(
modifier = modifier,
text = "10000",
radius = TangemTheme.dimens2.x25,
style = TextShimmerStyle.BODY,
)
}
}
}
@Composable
private fun Timestamp(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
TangemRowText(text = state.time, role = TangemRowTextRole.Subvalue)
}
is TangemPayTransactionState.Loading -> {
TextShimmer(
modifier = modifier,
text = "00:00",
radius = TangemTheme.dimens2.x25,
style = TextShimmerStyle.CAPTION,
)
}
}
}
@Composable
private fun TangemPayTransactionEmptyBlock(
state: TangemPayEmptyTransactionHistoryStateV2.Empty,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.testTag(EmptyTransactionBlockTestTags.BLOCK),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens2.x10)
.background(
color = TangemTheme.colors3.bg.opaque.primary,
shape = CircleShape,
)
.padding(10.dp)
.testTag(EmptyTransactionBlockTestTags.ICON),
imageVector = Icons.ic_binoculars_20,
tint = TangemTheme.colors3.icon.secondary,
contentDescription = null,
)
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing32)
.testTag(EmptyTransactionBlockTestTags.TEXT),
textAlign = TextAlign.Center,
text = state.text.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
@Composable
private fun TangemPayFailedTransactionBlock(
state: TangemPayEmptyTransactionHistoryStateV2.FailedToLoad,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.testTag(EmptyTransactionBlockTestTags.BLOCK),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_arrow_refresh_20),
onClick = state.onReload,
)
Text(
modifier = Modifier.testTag(EmptyTransactionBlockTestTags.TEXT),
textAlign = TextAlign.Center,
text = state.text.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
/**
* Computes the height left between the top of the item identified by [itemKey] and the bottom of the
* list's viewport (excluding bottom content padding). Returns `0.dp` until the item has been laid out.
*
* The item's own height does not affect its offset (only the items above it do), so reading the offset
* back to size the item is stable and does not loop.
*/
@Composable
private fun rememberRemainingViewportHeight(listState: LazyListState, itemKey: Any): Dp {
val density = LocalDensity.current
val remainingPx by remember(listState, itemKey) {
derivedStateOf {
val info = listState.layoutInfo
val item = info.visibleItemsInfo.firstOrNull { it.key == itemKey }
?: return@derivedStateOf 0
(info.viewportEndOffset - info.afterContentPadding - item.offset).coerceAtLeast(minimumValue = 0)
}
}
return with(density) { remainingPx.toDp() }
}