Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-04 17:10:01 +05:00
parent 2471b9c1a8
commit 941863f79b
18 changed files with 334 additions and 43 deletions

View file

@ -525,8 +525,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
// Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs
if (tangemPayFeatureToggles.isTangemPayEnabled) {
store.dispatchNavigationAction {
replaceAll(AppRoute.TangemPayDetails)
lifecycleScope.launch {
val selectedUserWalledId = userWalletsListRepository.selectedUserWalletSync()?.walletId
store.dispatchNavigationAction {
replaceAll(AppRoute.TangemPayDetails(requireNotNull(selectedUserWalledId)))
}
}
} else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {

View file

@ -591,7 +591,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(),
params = TangemPayDetailsComponent.Params(userWalletId = route.userWalletId),
componentFactory = tangemPayDetailsComponentFactory,
)
}

View file

@ -369,5 +369,5 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/archived_account/${userWalletId.stringValue}")
@Serializable
data object TangemPayDetails : AppRoute(path = "/tangem_pay_details")
data class TangemPayDetails(val userWalletId: UserWalletId) : AppRoute(path = "/tangem_pay_details")
}

View file

@ -23,7 +23,7 @@ import java.util.UUID
* @param modifier modifier
*/
@Composable
internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) {
fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(TangemTheme.colors.background.primary)

View file

@ -13,6 +13,9 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Domain */
implementation(projects.domain.models)
/** Compose */
implementation(deps.compose.runtime)
}

View file

@ -2,9 +2,9 @@ package com.tangem.features.tangempay.components
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface TangemPayDetailsComponent : ComposableContentComponent {
@Suppress("EmptyDefaultConstructor") // Will add params in Next PRs
class Params()
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, TangemPayDetailsComponent>
}

View file

@ -19,6 +19,12 @@ dependencies {
/** Features api */
implementation(projects.features.tangempay.details.api)
implementation(projects.features.txhistory.api)
/** Domain */
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.models)
/** Compose */
implementation(deps.compose.foundation)

View file

@ -5,8 +5,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.ui.TangemPayDetailsScreen
import dagger.assisted.Assisted
@ -19,12 +21,16 @@ internal class DefaultTangemPayDetailsComponent @AssistedInject constructor(
) : AppComponentContext by appComponentContext, TangemPayDetailsComponent {
private val model: TangemPayDetailsModel = getOrCreateModel(params = params)
private val txHistoryComponent = DefaultTangemPayTxHistoryComponent(
appComponentContext = child("txHistoryComponent"),
params = DefaultTangemPayTxHistoryComponent.Params(userWalletId = params.userWalletId),
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
TangemPayDetailsScreen(state = state, modifier = modifier)
TangemPayDetailsScreen(state = state, txHistoryComponent = txHistoryComponent, modifier = modifier)
}
@AssistedFactory

View file

@ -0,0 +1,26 @@
package com.tangem.features.tangempay.components.txHistory
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.ui.txHistoryItems
import kotlinx.coroutines.flow.StateFlow
internal class DefaultTangemPayTxHistoryComponent(
appComponentContext: AppComponentContext,
params: Params,
) : AppComponentContext by appComponentContext, TangemPayTxHistoryComponent {
private val model: TangemPayTxHistoryModel = getOrCreateModel(params = params)
override val state: StateFlow<TxHistoryUM> = model.uiState
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
txHistoryItems(listState, state)
}
data class Params(val userWalletId: UserWalletId)
}

View file

@ -0,0 +1,174 @@
package com.tangem.features.tangempay.components.txHistory
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.ui.txHistoryItems
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TxHistoryUM) : TangemPayTxHistoryComponent {
override val state: StateFlow<TxHistoryUM> = MutableStateFlow(txHistoryUM)
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
txHistoryItems(listState, state)
}
companion object {
val loadingUM = TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = {})
val emptyUM = TxHistoryUM.Empty(isBalanceHidden = true, onExploreClick = {})
val contentUM = TxHistoryUM.Content(
isBalanceHidden = false,
loadMore = { false },
items = persistentListOf(
TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = {}),
TxHistoryUM.TxHistoryItemUM.GroupTitle(title = "Today", itemKey = "Today"),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-4.99 USD",
time = "16:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-126.20 USD",
time = "12:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
),
),
TxHistoryUM.TxHistoryItemUM.GroupTitle(title = "Yesterday", itemKey = "Yesterday"),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-4.99 USD",
time = "21:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-126.20 USD",
time = "10:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-4.99 USD",
time = "19:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-126.20 USD",
time = "18:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-4.99 USD",
time = "17:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-126.20 USD",
time = "16:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-4.99 USD",
time = "15:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
amount = "-126.20 USD",
time = "14:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
),
),
),
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.tangempay.components.txHistory
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.features.txhistory.entity.TxHistoryUM
import kotlinx.coroutines.flow.StateFlow
internal interface TangemPayTxHistoryComponent {
val state: StateFlow<TxHistoryUM>
fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.tangempay.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -16,5 +17,10 @@ internal interface TangemPayModelModule {
@Binds
@IntoMap
@ClassKey(TangemPayDetailsModel::class)
fun bindWcConnectionsModel(model: TangemPayDetailsModel): Model
fun bindTangemPayDetailsModel(model: TangemPayDetailsModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayTxHistoryModel::class)
fun bindTangemPayTxHistoryModel(model: TangemPayTxHistoryModel): Model
}

View file

@ -0,0 +1,62 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayTxHistoryModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
) : Model() {
private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require()
val uiState: StateFlow<TxHistoryUM>
field = MutableStateFlow(getInitialState())
init {
handleBalanceHiding()
subscribeToUiItemChanges()
}
@Suppress("MagicNumber")
private fun subscribeToUiItemChanges() {
modelScope.launch {
Timber.d("subscribeToUiItemChanges: ${params.userWalletId}")
delay(2000)
uiState.update { PreviewTangemPayTxHistoryComponent.contentUM }
}
}
private fun handleBalanceHiding() {
getBalanceHidingSettingsUseCase()
.onEach { uiState.update { state -> state.copySealed(isBalanceHidden = it.isBalanceHidden) } }
.launchIn(modelScope)
}
private fun onExploreClick() {
Timber.d("onExploreClick: open explorer")
}
private fun getInitialState(): TxHistoryUM {
return TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = ::onExploreClick)
}
}

View file

@ -19,11 +19,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
@ -37,6 +39,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
@ -46,7 +50,11 @@ import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifier = Modifier) {
internal fun TangemPayDetailsScreen(
state: TangemPayDetailsUM,
txHistoryComponent: TangemPayTxHistoryComponent,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = { TangemPayDetailsTopAppBar(config = state.topBarConfig) },
@ -55,6 +63,7 @@ internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifie
) { scaffoldPaddings ->
val listState = rememberLazyListState()
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val txHistoryState by txHistoryComponent.state.collectAsStateWithLifecycle()
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
@ -91,6 +100,8 @@ internal fun TangemPayDetailsScreen(state: TangemPayDetailsUM, modifier: Modifie
)
},
)
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
}
}
}
@ -317,14 +328,19 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(device = Devices.PIXEL_7_PRO, group = "day")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO, group = "night")
@Composable
private fun TangemPayDetailsScreenPreview(
@PreviewParameter(TangemPayDetailsUMProvider::class) state: TangemPayDetailsUM,
) {
TangemThemePreview {
TangemPayDetailsScreen(state = state)
TangemPayDetailsScreen(
state = state,
txHistoryComponent = PreviewTangemPayTxHistoryComponent(
txHistoryUM = PreviewTangemPayTxHistoryComponent.contentUM,
),
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.txhistory.entity
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -81,7 +82,9 @@ sealed interface TxHistoryUM {
data class GroupTitle(
val title: String,
val itemKey: String,
) : TxHistoryItemUM
) : TxHistoryItemUM {
val legacyGroupTitle = TxHistoryState.TxHistoryItemState.GroupTitle(title = title, itemKey = itemKey)
}
/**
* Transaction item

View file

@ -1,20 +1,16 @@
package com.tangem.features.txhistory.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.transactions.PendingTxsBlock
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle
import com.tangem.core.ui.components.transactions.TxHistoryTitle
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
@ -25,7 +21,7 @@ import com.tangem.features.txhistory.entity.TxHistoryUM
private const val LOAD_ITEMS_BUFFER = 20
internal fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) {
fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) {
when (state) {
is TxHistoryUM.Content -> contentItems(listState, state)
is TxHistoryUM.Empty -> nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick))
@ -127,7 +123,7 @@ internal fun TxHistoryListItem(
) {
when (state) {
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> {
TxHistoryGroupTitle(config = state, modifier = modifier)
TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier)
}
is TxHistoryUM.TxHistoryItemUM.Title -> {
TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier)
@ -140,25 +136,4 @@ internal fun TxHistoryListItem(
)
}
}
}
@Composable
private fun TxHistoryGroupTitle(config: TxHistoryUM.TxHistoryItemUM.GroupTitle, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size24),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = config.title,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.features.txhistory.component
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.txhistory.entity.TxHistoryUM

View file

@ -29,6 +29,7 @@ internal fun BaseExtension.configureCompose(project: Project) {
contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA]
contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function
contains(Regex(pattern = ":features:txhistory:api\$")) || // provides Composable function
contains(Regex(pattern = ":impl\$"))
}