Updated on 2026-08-14
This commit is contained in:
parent
bbe46b2ab5
commit
4383291669
9 changed files with 310 additions and 0 deletions
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TxHistoryDetailsComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val txInfo: Flow<TxInfo>,
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TxHistoryDetailsComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.features.txhistory.model.TxHistoryDetailsModel
|
||||
import com.tangem.features.txhistory.ui.TxHistoryDetailsContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultTxHistoryDetailsComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: TxHistoryDetailsComponent.Params,
|
||||
) : TxHistoryDetailsComponent, AppComponentContext by context {
|
||||
|
||||
private val model: TxHistoryDetailsModel = getOrCreateModel(params)
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
TangemModalBottomSheet<TxHistoryDetailsUM>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = state != null,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = state ?: TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = ::dismiss)
|
||||
},
|
||||
content = { um -> TxHistoryDetailsContent(state = um) },
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TxHistoryDetailsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: TxHistoryDetailsComponent.Params,
|
||||
): DefaultTxHistoryDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.network.TxInfo.TransactionType
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts a [TxInfo] to a [TxHistoryDetailsUM] for the in-app transaction details card.
|
||||
*
|
||||
* Single dispatch on [TxInfo.type] picks the layout family — mirroring the same `when(type)` used by
|
||||
* [TxHistoryItemToTransactionItemUMConverter]:
|
||||
* - [TransactionType.Swap] (and onramp once it lands in `TxInfo`) -> [TxHistoryDetailsUM.TwoAssets]
|
||||
* - everything else -> [TxHistoryDetailsUM.SingleAsset]
|
||||
*/
|
||||
internal class TxInfoToTxHistoryDetailsUMConverter : Converter<TxInfo, TxHistoryDetailsUM> {
|
||||
|
||||
override fun convert(value: TxInfo): TxHistoryDetailsUM = when (value.type) {
|
||||
is TransactionType.Swap -> twoAssets(value)
|
||||
else -> singleAsset(value)
|
||||
}
|
||||
|
||||
private fun singleAsset(tx: TxInfo): TxHistoryDetailsUM.SingleAsset = TxHistoryDetailsUM.SingleAsset(
|
||||
title = tx.type.toString(),
|
||||
)
|
||||
|
||||
private fun twoAssets(tx: TxInfo): TxHistoryDetailsUM.TwoAssets = TxHistoryDetailsUM.TwoAssets(
|
||||
title = tx.type.toString(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.txhistory.model.TxHistoryDetailsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface TxHistoryDetailsModelModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TxHistoryDetailsModel::class)
|
||||
fun bindModel(model: TxHistoryDetailsModel): Model
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.features.txhistory.component.DefaultTxHistoryComponent
|
||||
import com.tangem.features.txhistory.component.DefaultTxHistoryDetailsComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryDetailsComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,4 +17,10 @@ internal interface TxHistoryFeatureModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(factory: DefaultTxHistoryComponent.Factory): TxHistoryComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTxHistoryDetailsComponentFactory(
|
||||
factory: DefaultTxHistoryDetailsComponent.Factory,
|
||||
): TxHistoryDetailsComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
/**
|
||||
* UI model for the in-app transaction details ("Operation") card.
|
||||
*
|
||||
* One model for all transaction types; the layout family is chosen from the transaction type by
|
||||
* `TxInfoToTxHistoryDetailsUMConverter`:
|
||||
* - [SingleAsset] — Receive / Send / Transfer
|
||||
* - [TwoAssets] — Swap / Onramp
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
|
||||
|
||||
/** Operation title, status-driven color is resolved at render time. */
|
||||
val title: String
|
||||
|
||||
/** Single-asset layout: Receive / Send / Transfer */
|
||||
data class SingleAsset(
|
||||
override val title: String,
|
||||
) : TxHistoryDetailsUM
|
||||
|
||||
/** Two-asset layout: Swap / Onramp */
|
||||
data class TwoAssets(
|
||||
override val title: String,
|
||||
) : TxHistoryDetailsUM
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.txhistory.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.features.txhistory.component.TxHistoryDetailsComponent
|
||||
import com.tangem.features.txhistory.converter.TxInfoToTxHistoryDetailsUMConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TxHistoryDetailsModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: TxHistoryDetailsComponent.Params = paramsContainer.require()
|
||||
|
||||
private val converter = TxInfoToTxHistoryDetailsUMConverter()
|
||||
|
||||
val uiState: StateFlow<TxHistoryDetailsUM?> = params.txInfo
|
||||
.map(converter::convert)
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(modelScope, SharingStarted.WhileSubscribed(), initialValue = null)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
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.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
|
||||
@Composable
|
||||
internal fun TxHistoryDetailsContent(state: TxHistoryDetailsUM, modifier: Modifier = Modifier) {
|
||||
// Placeholder:card showing only the operation title, to verify tap -> sheet navigation
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.heightIn(min = 240.dp)
|
||||
.padding(TangemTheme.dimens2.x6),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.title,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
style = TangemTheme.typography2.headingSemibold28,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.network.TxInfo.TransactionType
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TxInfoToTxHistoryDetailsUMConverterTest {
|
||||
|
||||
private val converter = TxInfoToTxHistoryDetailsUMConverter()
|
||||
|
||||
@Test
|
||||
fun `GIVEN Swap WHEN convert THEN TwoAssets`() {
|
||||
// Arrange
|
||||
val tx = txInfo(type = TransactionType.Swap)
|
||||
|
||||
// Act
|
||||
val result = converter.convert(tx)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.TwoAssets::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Swap TransactionType WHEN convert THEN SingleAsset`() {
|
||||
val nonSwapTypes = listOf(
|
||||
TransactionType.Transfer,
|
||||
TransactionType.Approve,
|
||||
TransactionType.Operation(name = "Mint NFT"),
|
||||
TransactionType.UnknownOperation,
|
||||
TransactionType.GaslessFee,
|
||||
TransactionType.Staking.Stake,
|
||||
TransactionType.Staking.ClaimRewards,
|
||||
TransactionType.Staking.Vote(validatorAddress = VALIDATOR_ADDRESS),
|
||||
TransactionType.YieldSupply.Topup,
|
||||
TransactionType.YieldSupply.Enter(address = USER_ADDRESS),
|
||||
)
|
||||
|
||||
nonSwapTypes.forEach { type ->
|
||||
val result = converter.convert(txInfo(type = type))
|
||||
|
||||
assertThat(result).isInstanceOf(TxHistoryDetailsUM.SingleAsset::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tx WHEN convert THEN title is the transaction type`() {
|
||||
// Arrange
|
||||
val type = TransactionType.Transfer
|
||||
val tx = txInfo(type = type)
|
||||
|
||||
// Act
|
||||
val result = converter.convert(tx)
|
||||
|
||||
// Assert
|
||||
assertThat(result.title).isEqualTo(type.toString())
|
||||
}
|
||||
|
||||
private fun txInfo(type: TransactionType): TxInfo = TxInfo(
|
||||
txHash = TX_HASH,
|
||||
timestampInMillis = TIMESTAMP,
|
||||
isOutgoing = false,
|
||||
destinationType = TxInfo.DestinationType.Single(addressType = TxInfo.AddressType.User(USER_ADDRESS)),
|
||||
sourceType = TxInfo.SourceType.Single(address = USER_ADDRESS),
|
||||
interactionAddressType = null,
|
||||
status = TxInfo.TransactionStatus.Confirmed,
|
||||
type = type,
|
||||
amount = BigDecimal.ONE,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val TX_HASH = "0xtxhash"
|
||||
const val TIMESTAMP = 1_700_000_000_000L
|
||||
const val USER_ADDRESS = "0x1234567890abcdef1234"
|
||||
const val VALIDATOR_ADDRESS = "0xvalidator"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue