diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt index d27e63ad51..dfe30677d0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt @@ -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 = 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( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt index 7f04f563e2..a35f99e1fc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt @@ -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), ), ), ), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt index 50816ac2a7..0bec7f83be 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt @@ -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) + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt index f14463cd0c..ad7c6b64e6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt @@ -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 } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt index 4bc4f8207b..1441088115 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt @@ -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" + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt index 47b229ce5d..5cd1ea401f 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -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) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt index 7ce0c8be84..8b99f71a4d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt @@ -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 }, + ), ) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt index c22493816b..0c5053794b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt @@ -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) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUiV2.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUiV2.kt new file mode 100644 index 0000000000..0d0f482d92 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUiV2.kt @@ -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() } +} \ No newline at end of file