Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-20 14:14:10 +04:00
parent d808a3ddb2
commit b775329fb3
28 changed files with 768 additions and 73 deletions

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.expressStatus.state
import androidx.compose.runtime.Composable
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.PersistentList
data class ExpressTransactionsBlockState(
val transactions: PersistentList<ExpressTransactionStateUM>,
val bottomSheetSlot: BottomSheetSlot?,
val dialogSlot: DialogSlot?,
)
data class BottomSheetSlot(
val config: TangemBottomSheetConfig,
val content: @Composable () -> Unit,
)
data class DialogSlot(
val config: TokenDetailsDialogConfig,
val content: @Composable () -> Unit,
)

View file

@ -1,9 +1,9 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
package com.tangem.common.ui.tokendetails
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.tokendetails.impl.R
/**
* Wallet bottom sheet config
@ -12,7 +12,7 @@ import com.tangem.features.tokendetails.impl.R
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
internal data class TokenDetailsDialogConfig(
data class TokenDetailsDialogConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: DialogContentConfig,
@ -28,7 +28,7 @@ internal data class TokenDetailsDialogConfig(
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val warning: Boolean = false,
val hasWarning: Boolean = false,
)
data class ConfirmHideConfig(
@ -51,7 +51,7 @@ internal data class TokenDetailsDialogConfig(
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.token_details_hide_alert_hide),
onClick = onConfirmClick,
warning = true,
hasWarning = true,
)
}

View file

@ -29,6 +29,7 @@ dependencies {
implementation(projects.features.tangempay.details.api)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.txhistory.api)
implementation(projects.features.tokendetails.api)
/** Domain */
implementation(projects.domain.balanceHiding)

View file

@ -15,16 +15,18 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tangempay.components.express.ExpressTransactionsComponentProvider
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
import com.tangem.features.tokenreceive.TokenReceiveComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor(
internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: TangemPayDetailsContainerComponent.Params,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val expressTransactionsComponentProvider: ExpressTransactionsComponentProvider,
) : AppComponentContext by appComponentContext, TangemPayDetailsContainerComponent {
private val stackNavigation = StackNavigation<TangemPayDetailsInnerRoute>()
@ -60,6 +62,7 @@ class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
params = params,
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
expressTransactionsComponentProvider = expressTransactionsComponentProvider,
)
TangemPayDetailsInnerRoute.ChangePIN -> TangemPayChangePinComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),

View file

@ -8,6 +8,7 @@ import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
@ -17,6 +18,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.express.ExpressTransactionsComponentProvider
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
@ -28,6 +30,7 @@ internal class TangemPayDetailsComponent(
private val appComponentContext: AppComponentContext,
private val params: TangemPayDetailsContainerComponent.Params,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val expressTransactionsComponentProvider: ExpressTransactionsComponentProvider,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: TangemPayDetailsModel = getOrCreateModel(params = params)
@ -52,6 +55,21 @@ internal class TangemPayDetailsComponent(
params = TangemPayCardDetailsBlockComponent.Params(params = params),
)
private val expressTransactionsComponent by lazy {
expressTransactionsComponentProvider.create(
appComponentContext = child("expressTransactionsComponent"),
userWalletId = params.userWalletId,
cryptoCurrency = model.cryptoCurrency,
)
}
init {
lifecycle.subscribe(
onPause = model::onPause,
onResume = model::onResume,
)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
@ -62,6 +80,7 @@ internal class TangemPayDetailsComponent(
state = state,
txHistoryComponent = txHistoryComponent,
cardDetailsBlockComponent = cardDetailsBlockComponent,
expressTransactionsComponent = expressTransactionsComponent,
modifier = modifier,
)
bottomSheet.child?.instance?.BottomSheet()

View file

@ -0,0 +1,34 @@
package com.tangem.features.tangempay.components.express
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class EmptyExpressTransactionsComponent(
context: AppComponentContext,
) : AppComponentContext by context, ExpressTransactionsComponent {
override val state: StateFlow<ExpressTransactionsBlockState> = MutableStateFlow(getInitialState())
override fun LazyListScope.expressTransactionsContent(
state: PersistentList<ExpressTransactionStateUM>,
modifier: Modifier,
) {}
private fun getInitialState(): ExpressTransactionsBlockState {
return ExpressTransactionsBlockState(
transactions = persistentListOf(),
bottomSheetSlot = null,
dialogSlot = null,
)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.tangempay.components.express
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import javax.inject.Inject
internal class ExpressTransactionsComponentProvider @Inject constructor(
private val expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
) {
fun create(
appComponentContext: AppComponentContext,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency?,
): ExpressTransactionsComponent = if (cryptoCurrency != null) {
expressTransactionsComponentFactory.create(
context = appComponentContext,
params = ExpressTransactionsComponent.Params(userWalletId = userWalletId, currency = cryptoCurrency),
)
} else {
EmptyExpressTransactionsComponent(context = appComponentContext)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.tangempay.components.express
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/** Cannot really preview anything here since the UM implementation [ExchangeUM] is in token:details module
* For the actual preview @see [TokenDetailsScreen]
**/
internal class PreviewEmptyExpressTransactionsComponent : ExpressTransactionsComponent {
override val state: StateFlow<ExpressTransactionsBlockState> = MutableStateFlow(getInitialState())
override fun LazyListScope.expressTransactionsContent(
state: PersistentList<ExpressTransactionStateUM>,
modifier: Modifier,
) {}
private fun getInitialState(): ExpressTransactionsBlockState {
return ExpressTransactionsBlockState(
transactions = persistentListOf(),
bottomSheetSlot = null,
dialogSlot = null,
)
}
}

View file

@ -21,6 +21,8 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayTopUpData
@ -47,6 +49,8 @@ import com.tangem.features.tangempay.utils.TangemPayDetailIntents
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUpdateListener
import com.tangem.features.tokendetails.ExpressTransactionsEvent
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -78,6 +82,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val expressTransactionsEventListener: ExpressTransactionsEventListener,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener, ViewPinListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -99,6 +104,11 @@ internal class TangemPayDetailsModel @Inject constructor(
private var balance: TangemPayCardBalance? = null
private val userWallet: UserWallet? = getUserWalletUseCase(params.userWalletId).getOrNull()
val cryptoCurrency: CryptoCurrency? = userWallet?.let { wallet ->
tangemPayCryptoCurrencyFactory.create(userWallet = wallet, chainId = params.config.chainId).getOrNull()
}
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
init {
@ -109,6 +119,18 @@ internal class TangemPayDetailsModel @Inject constructor(
subscribeToCardFrozenState()
}
fun onResume() {
modelScope.launch {
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
}
}
fun onPause() {
modelScope.launch {
expressTransactionsEventListener.send(ExpressTransactionsEvent.Clear)
}
}
private fun subscribeToCardFrozenState() {
cardDetailsRepository
.cardFrozenState(params.config.cardId)
@ -252,8 +274,8 @@ internal class TangemPayDetailsModel @Inject constructor(
if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else {
val userWallet = getUserWalletUseCase(params.userWalletId).getOrNull()
val currency = userWallet?.let {
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
val currency = cryptoCurrency ?: userWallet?.let {
tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId)
.getOrNull()
}
@ -336,6 +358,7 @@ internal class TangemPayDetailsModel @Inject constructor(
modelScope.launch {
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value))
cardDetailsEventListener.send(CardDetailsEvent.Hide)
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
txHistoryUpdateListener.triggerUpdate()
fetchBalance().join()
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false))

View file

@ -34,17 +34,22 @@ import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTransactionsComponent
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.*
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
@ -54,6 +59,7 @@ internal fun TangemPayDetailsScreen(
state: TangemPayDetailsUM,
txHistoryComponent: TangemPayTxHistoryComponent,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
expressTransactionsComponent: ExpressTransactionsComponent,
modifier: Modifier = Modifier,
) {
Scaffold(
@ -66,6 +72,9 @@ internal fun TangemPayDetailsScreen(
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val txHistoryState by txHistoryComponent.state.collectAsStateWithLifecycle()
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle()
val expressTransactionsBottomSheetState = expressState.bottomSheetSlot
val expressTransactionsDialogState = expressState.dialogSlot
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
@ -138,9 +147,19 @@ internal fun TangemPayDetailsScreen(
)
},
)
with(expressTransactionsComponent) {
expressTransactionsContent(
state = expressState.transactions,
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
.fillMaxWidth(),
)
}
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
}
}
expressTransactionsDialogState?.content()
expressTransactionsBottomSheetState?.content()
}
}
@ -292,6 +311,7 @@ private fun TangemPayDetailsScreenPreview(
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
),
),
expressTransactionsComponent = PreviewEmptyExpressTransactionsComponent(),
)
}
}
@ -370,6 +390,7 @@ private fun TangemPayDetailsTxHistoryScreenPreview(
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
),
),
expressTransactionsComponent = PreviewEmptyExpressTransactionsComponent(),
)
}
}

View file

@ -14,6 +14,9 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Common */
implementation(projects.common.ui)
/** Domain models */
api(projects.domain.models)
implementation(projects.domain.tokens.models)
@ -21,4 +24,9 @@ dependencies {
/** Compose */
implementation(deps.compose.runtime)
implementation(deps.compose.ui)
implementation(deps.compose.foundation)
/** Other */
implementation(deps.kotlin.immutable.collections)
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.tokendetails
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.flow.StateFlow
@Stable
interface ExpressTransactionsComponent {
val state: StateFlow<ExpressTransactionsBlockState>
fun LazyListScope.expressTransactionsContent(state: PersistentList<ExpressTransactionStateUM>, modifier: Modifier)
data class Params(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
)
interface Factory : ComponentFactory<Params, ExpressTransactionsComponent>
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.tokendetails
import kotlinx.coroutines.flow.Flow
interface ExpressTransactionsEventListener {
val event: Flow<ExpressTransactionsEvent>
suspend fun send(event: ExpressTransactionsEvent)
}
enum class ExpressTransactionsEvent {
Update, Clear
}

View file

@ -4,9 +4,14 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.feature.tokendetails.DefaultTokenDetailsComponent
import com.tangem.feature.tokendetails.presentation.DefaultExpressTransactionsComponent
import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.DefaultExpressTransactionsEventListener
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
import com.tangem.features.tokendetails.TokenDetailsComponent
import dagger.Binds
import dagger.Module
@ -14,18 +19,35 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface TokenDetailsModule {
@Binds
fun bindComponentFactory(factory: DefaultTokenDetailsComponent.Factory): TokenDetailsComponent.Factory
fun bindTokenDetailsComponentFactory(factory: DefaultTokenDetailsComponent.Factory): TokenDetailsComponent.Factory
@Binds
fun bindExpressTransactionsComponent(
factory: DefaultExpressTransactionsComponent.Factory,
): ExpressTransactionsComponent.Factory
@Binds
@IntoMap
@ClassKey(TokenDetailsModel::class)
fun bindModel(model: TokenDetailsModel): Model
fun bindTokenDetailsModel(model: TokenDetailsModel): Model
@Binds
@IntoMap
@ClassKey(ExpressTransactionsModel::class)
fun bindExpressTransactionsModel(model: ExpressTransactionsModel): Model
@Binds
@Singleton
fun bindExpressTransactionsEventListener(
impl: DefaultExpressTransactionsEventListener,
): ExpressTransactionsEventListener
}
@Module

View file

@ -0,0 +1,42 @@
package com.tangem.feature.tokendetails.presentation
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsModel
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.flow.StateFlow
@Stable
internal class DefaultExpressTransactionsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: ExpressTransactionsComponent.Params,
) : AppComponentContext by context, ExpressTransactionsComponent {
private val model: ExpressTransactionsModel = getOrCreateModel(params = params)
override val state: StateFlow<ExpressTransactionsBlockState> = model.uiState
override fun LazyListScope.expressTransactionsContent(
state: PersistentList<ExpressTransactionStateUM>,
modifier: Modifier,
) {
expressTransactionsItems(expressTxs = state, modifier = modifier)
}
@AssistedFactory
interface Factory : ExpressTransactionsComponent.Factory {
override fun create(
context: AppComponentContext,
params: ExpressTransactionsComponent.Params,
): DefaultExpressTransactionsComponent
}
}

View file

@ -0,0 +1,306 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.model
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import arrow.core.right
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.BottomSheetSlot
import com.tangem.common.ui.expressStatus.state.DialogSlot
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.tokendetails.ExpressTransactionsEvent
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
@Suppress("LongParameterList", "PropertyUsedBeforeDeclaration")
@Stable
@ModelScoped
internal class ExpressTransactionsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
expressStatusFactory: ExpressStatusFactory.Factory,
getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val router: InnerTokenDetailsRouter,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val expressTransactionsEventListener: ExpressTransactionsEventListener,
) : Model(), ExpressTransactionsClickIntents {
private val params = paramsContainer.require<ExpressTransactionsComponent.Params>()
private val userWalletId: UserWalletId = params.userWalletId
private val cryptoCurrency: CryptoCurrency = params.currency
private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found")
private val marketPriceJobHolder = JobHolder()
private val expressTxJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var account: Account.CryptoPortfolio? = null
private val expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
private val currentStateProvider: Provider<TokenDetailsState> = Provider { internalUiState.value }
private val stateFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
TokenDetailsStateFactory(
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
tokenDetailsClickIntents = EmptyTokenDetailsClickIntents(),
expressTransactionsClickIntents = this,
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
}
private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
expressStatusFactory.create(
clickIntents = this,
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
)
}
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
val uiState: StateFlow<ExpressTransactionsBlockState> = internalUiState
.map(::mapInnerState)
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = mapInnerState(internalUiState.value),
)
init {
subscribeOnExternalEvents()
subscribeOnCurrencyStatusUpdates()
subscribeOnExpressTransactionsUpdates()
}
override fun onExpressTransactionClick(txId: String) {
val expressTxState = internalUiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId }
?: return
internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState)
}
override fun onGoToProviderClick(url: String) {
router.openUrl(url)
}
override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) {
router.openTokenDetails(userWalletId, cryptoCurrency)
}
override fun onOpenUrlClick(url: String) {
router.openUrl(url)
}
override fun onConfirmDisposeExpressStatus() {
internalUiState.value = stateFactory.getStateWithConfirmHideExpressStatus()
}
override fun onDisposeExpressStatus() {
val bottomSheetState = internalUiState.value.bottomSheetConfig?.content
if (bottomSheetState is ExpressStatusBottomSheetConfig) {
modelScope.launch {
expressStatusFactory.removeTransactionOnBottomSheetClosed(
expressState = bottomSheetState.value,
isForceDispose = true,
)
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
}
override fun onDismissBottomSheet() {
when (val bsContent = internalUiState.value.bottomSheetConfig?.content) {
is ExpressStatusBottomSheetConfig -> {
modelScope.launch(dispatchers.main) {
expressStatusFactory.removeTransactionOnBottomSheetClosed(bsContent.value)
}
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
}
override fun onDismissDialog() {
internalUiState.value = stateFactory.getStateWithClosedDialog()
}
override fun onDestroy() {
clear()
super.onDestroy()
}
private fun mapInnerState(innerState: TokenDetailsState): ExpressTransactionsBlockState {
val bsContent = innerState.bottomSheetConfig?.content
return ExpressTransactionsBlockState(
transactions = innerState.expressTxsToDisplay,
bottomSheetSlot = if (bsContent != null && bsContent is ExpressStatusBottomSheetConfig) {
innerState.bottomSheetConfig.toBottomSheetSlot()
} else {
null
},
dialogSlot = innerState.dialogConfig?.toDialogSlot(),
)
}
private fun TangemBottomSheetConfig.toBottomSheetSlot(): BottomSheetSlot {
val contentLambda: @Composable () -> Unit = {
when (this.content) {
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = this)
}
}
return BottomSheetSlot(config = this, content = contentLambda)
}
private fun TokenDetailsDialogConfig.toDialogSlot(): DialogSlot {
val contentLambda: @Composable () -> Unit = {
TokenDetailsDialogs(this)
}
return DialogSlot(config = this, content = contentLambda)
}
private fun subscribeOnExternalEvents() {
modelScope.launch {
expressTransactionsEventListener.event.collect { event ->
when (event) {
ExpressTransactionsEvent.Update -> subscribeOnExpressTransactionsUpdates()
ExpressTransactionsEvent.Clear -> clear()
}
}
}
}
private fun subscribeOnCurrencyStatusUpdates() {
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
.onEach { account = it.account }
.map { it.status.right() }
} else {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
}
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
}
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(marketPriceJobHolder)
}
private fun subscribeOnExpressTransactionsUpdates() {
expressTxStatusTaskScheduler.cancelTask()
expressStatusFactory.getExpressStatuses()
.distinctUntilChanged()
.onEach { waitForFirstExpressStatusEmmit.value = true }
.onEach { expressTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
expressTxs = expressTxs,
updateBalance = { /* no-op */ },
)
expressTxStatusTaskScheduler.scheduleTask(
scope = modelScope,
task = PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
try {
Result.success(
expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs),
)
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Result.failure(exception)
}
},
onSuccess = { updatedTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
expressTxs = updatedTxs,
updateBalance = { /* no-op */ },
)
},
onError = { /* no-op */ },
),
)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(expressTxJobHolder)
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
}
private fun clear() {
expressTxStatusTaskScheduler.cancelTask()
expressTxJobHolder.cancel()
}
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}
}

View file

@ -24,8 +24,6 @@ interface TokenDetailsClickIntents {
fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onDismissDialog()
fun onHideClick()
fun onHideConfirmed()
@ -42,14 +40,8 @@ interface TokenDetailsClickIntents {
fun onAddressTypeSelected(addressModel: AddressModel)
fun onDismissBottomSheet()
fun onCloseRentInfoNotification()
fun onExpressTransactionClick(txId: String)
fun onGoToProviderClick(url: String)
fun onSwapPromoDismiss(promoId: PromoId)
fun onSwapPromoClick(promoId: PromoId)
@ -72,6 +64,15 @@ interface TokenDetailsClickIntents {
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
fun onYieldInfoClick()
}
interface ExpressTransactionsClickIntents {
fun onExpressTransactionClick(txId: String)
fun onGoToProviderClick(url: String)
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
fun onOpenUrlClick(url: String)
@ -80,5 +81,69 @@ interface TokenDetailsClickIntents {
fun onDisposeExpressStatus()
fun onYieldInfoClick()
fun onDismissBottomSheet()
fun onDismissDialog()
}
@Suppress("TooManyFunctions")
internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
override fun onRefreshSwipe(isRefreshing: Boolean) { /* no op */ }
override fun onBackClick() { /* no op */ }
override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) { /* no op */ }
override fun onStakeBannerClick() { /* no op */ }
override fun onReloadClick() { /* no op */ }
override fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onGenerateExtendedKey() { /* no op */ }
override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onHideClick() { /* no op */ }
override fun onHideConfirmed() { /* no op */ }
override fun onExploreClick() { /* no op */ }
override fun onAddressTypeSelected(addressModel: AddressModel) { /* no op */ }
override fun onTransactionClick(txHash: String) { /* no op */ }
override fun onCloseRentInfoNotification() { /* no op */ }
override fun onSwapPromoDismiss(promoId: PromoId) { /* no op */ }
override fun onSwapPromoClick(promoId: PromoId) { /* no op */ }
override fun onRetryIncompleteTransactionClick() { /* no op */ }
override fun onOpenTrustlineClick() { /* no op */ }
override fun onDismissIncompleteTransactionClick() { /* no op */ }
override fun onConfirmDismissIncompleteTransactionClick() { /* no op */ }
override fun onAssociateClick() { /* no op */ }
override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) { /* no op */ }
override fun onYieldInfoClick() { /* no op */ }
override fun onCopyAddress(): TextReference? {
/* no op */
return null
}
}

View file

@ -153,7 +153,10 @@ internal class TokenDetailsModel @Inject constructor(
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback {
) : Model(),
TokenDetailsClickIntents,
ExpressTransactionsClickIntents,
YieldSupplyDepositedWarningComponent.ModelCallback {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
private val userWalletId: UserWalletId = params.userWalletId
@ -184,7 +187,8 @@ internal class TokenDetailsModel @Inject constructor(
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
clickIntents = this,
tokenDetailsClickIntents = this,
expressTransactionsClickIntents = this,
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,

View file

@ -1,10 +1,10 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList

View file

@ -20,14 +20,14 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
internal class TokenDetailsOnrampTransactionStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val clickIntents: ExpressTransactionsClickIntents,
private val cryptoCurrency: CryptoCurrency,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.Either
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.common.ui.tokens.getUnavailabilityReasonText
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
@ -27,12 +28,12 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.Provider
@ -43,7 +44,8 @@ internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val clickIntents: TokenDetailsClickIntents,
private val tokenDetailsClickIntents: TokenDetailsClickIntents,
private val expressTransactionsClickIntents: ExpressTransactionsClickIntents,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val userWalletId: UserWalletId,
@ -52,7 +54,7 @@ internal class TokenDetailsStateFactory(
private val skeletonStateConverter by lazy {
TokenDetailsSkeletonStateConverter(
clickIntents = clickIntents,
clickIntents = tokenDetailsClickIntents,
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,
@ -64,7 +66,7 @@ internal class TokenDetailsStateFactory(
TokenDetailsNotificationConverter(
userWalletId = userWalletId,
getUserWalletUseCase = getUserWalletUseCase,
clickIntents = clickIntents,
clickIntents = tokenDetailsClickIntents,
)
}
@ -72,7 +74,7 @@ internal class TokenDetailsStateFactory(
TokenDetailsLoadedBalanceConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
clickIntents = clickIntents,
clickIntents = tokenDetailsClickIntents,
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
}
@ -80,7 +82,7 @@ internal class TokenDetailsStateFactory(
private val tokenDetailsButtonsConverter by lazy {
TokenDetailsActionButtonsConverter(
currentStateProvider = currentStateProvider,
clickIntents = clickIntents,
clickIntents = tokenDetailsClickIntents,
)
}
@ -117,7 +119,7 @@ internal class TokenDetailsStateFactory(
return TokenDetailsStakingInfoConverter(
currentState = state,
cryptoCurrencyStatus = cryptoCurrencyStatus,
clickIntents = clickIntents,
clickIntents = tokenDetailsClickIntents,
appCurrencyProvider = appCurrencyProvider,
stakingEntryInfo = stakingEntryInfo,
).convert(stakingAvailability)
@ -136,11 +138,11 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
onDismissRequest = expressTransactionsClickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig(
currencyTitle = currency.name,
onConfirmClick = clickIntents::onHideConfirmed,
onCancelClick = clickIntents::onDismissDialog,
onConfirmClick = tokenDetailsClickIntents::onHideConfirmed,
onCancelClick = expressTransactionsClickIntents::onDismissDialog,
),
),
)
@ -150,12 +152,12 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
onDismissRequest = expressTransactionsClickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig(
currencyName = currency.name,
currencySymbol = currency.symbol,
networkName = currency.network.name,
onConfirmClick = clickIntents::onDismissDialog,
onConfirmClick = expressTransactionsClickIntents::onDismissDialog,
),
),
)
@ -165,10 +167,10 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
onDismissRequest = expressTransactionsClickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.RemoveIncompleteTransactionConfirmDialogConfig(
onConfirmClick = clickIntents::onConfirmDismissIncompleteTransactionClick,
onCancelClick = clickIntents::onDismissDialog,
onConfirmClick = tokenDetailsClickIntents::onConfirmDismissIncompleteTransactionClick,
onCancelClick = expressTransactionsClickIntents::onDismissDialog,
),
),
)
@ -178,10 +180,10 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
onDismissRequest = expressTransactionsClickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig(
text = unavailabilityReason.getUnavailabilityReasonText(),
onConfirmClick = clickIntents::onDismissDialog,
onConfirmClick = expressTransactionsClickIntents::onDismissDialog,
),
),
)
@ -191,10 +193,10 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
onDismissRequest = expressTransactionsClickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.ErrorDialogConfig(
text = text,
onConfirmClick = clickIntents::onDismissDialog,
onConfirmClick = expressTransactionsClickIntents::onDismissDialog,
),
),
)
@ -217,7 +219,7 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
onDismissRequest = expressTransactionsClickIntents::onDismissBottomSheet,
content = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = currency.name,
@ -240,7 +242,7 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
onDismissRequest = expressTransactionsClickIntents::onDismissBottomSheet,
content = ChooseAddressBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = currency.name,
@ -248,7 +250,7 @@ internal class TokenDetailsStateFactory(
),
network = currency.network,
networkAddress = networkAddress,
onClick = clickIntents::onAddressTypeSelected,
onClick = tokenDetailsClickIntents::onAddressTypeSelected,
),
),
)
@ -338,13 +340,13 @@ internal class TokenDetailsStateFactory(
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
onDismissRequest = expressTransactionsClickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig(
onConfirmClick = {
clickIntents.onDisposeExpressStatus()
clickIntents.onDismissDialog()
expressTransactionsClickIntents.onDisposeExpressStatus()
expressTransactionsClickIntents.onDismissDialog()
},
onCancelClick = clickIntents::onDismissDialog,
onCancelClick = expressTransactionsClickIntents::onDismissDialog,
),
),
)
@ -367,13 +369,13 @@ internal class TokenDetailsStateFactory(
TangemDropdownMenuItem(
title = resourceReference(R.string.token_details_generate_xpub),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = clickIntents::onGenerateExtendedKey,
onClick = tokenDetailsClickIntents::onGenerateExtendedKey,
).let(::add)
}
TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColor = themedColor { TangemTheme.colors.text.warning },
onClick = clickIntents::onHideClick,
onClick = tokenDetailsClickIntents::onHideClick,
).let(::add)
}.toImmutableList(),
)

View file

@ -25,7 +25,7 @@ import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isF
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@ -43,7 +43,7 @@ import java.util.Locale
// Fixme [REDACTED_JIRA]
@Suppress("LargeClass")
internal class TokenDetailsSwapTransactionsStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val clickIntents: ExpressTransactionsClickIntents,
private val cryptoCurrency: CryptoCurrency,
private val analyticsEventsHandler: AnalyticsEventHandler,
appCurrencyProvider: Provider<AppCurrency>,

View file

@ -18,7 +18,7 @@ import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
@ -44,7 +44,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val swapTransactionStatusStore: SwapTransactionStatusStore,
private val analyticsEventsHandler: AnalyticsEventHandler,
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val clickIntents: ExpressTransactionsClickIntents,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val userWallet: UserWallet,
@ -257,7 +257,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(
clickIntents: TokenDetailsClickIntents,
clickIntents: ExpressTransactionsClickIntents,
appCurrencyProvider: Provider<AppCurrency>,
currentStateProvider: Provider<TokenDetailsState>,
userWallet: UserWallet,

View file

@ -12,7 +12,7 @@ import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@ -33,7 +33,7 @@ import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class ExpressStatusFactory @AssistedInject constructor(
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val clickIntents: ExpressTransactionsClickIntents,
@Assisted private val cryptoCurrency: CryptoCurrency,
@Assisted appCurrencyProvider: Provider<AppCurrency>,
@Assisted userWallet: UserWallet,
@ -205,7 +205,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
interface Factory {
@Suppress("LongParameterList")
fun create(
clickIntents: TokenDetailsClickIntents,
clickIntents: ExpressTransactionsClickIntents,
appCurrencyProvider: Provider<AppCurrency>,
currentStateProvider: Provider<TokenDetailsState>,
userWallet: UserWallet,

View file

@ -15,7 +15,7 @@ import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampStatus.Status.*
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter
import com.tangem.utils.Provider
@ -37,7 +37,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
@Assisted private val currentStateProvider: Provider<TokenDetailsState>,
@Assisted private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@Assisted private val clickIntents: TokenDetailsClickIntents,
@Assisted private val clickIntents: ExpressTransactionsClickIntents,
@Assisted private val cryptoCurrency: CryptoCurrency,
@Assisted private val userWallet: UserWallet,
) {
@ -157,7 +157,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
currentStateProvider: Provider<TokenDetailsState>,
cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
appCurrencyProvider: Provider<AppCurrency>,
clickIntents: TokenDetailsClickIntents,
clickIntents: ExpressTransactionsClickIntents,
cryptoCurrency: CryptoCurrency,
userWallet: UserWallet,
): OnrampStatusFactory

View file

@ -0,0 +1,24 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils
import com.tangem.features.tokendetails.ExpressTransactionsEvent
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class DefaultExpressTransactionsEventListener @Inject constructor() : ExpressTransactionsEventListener {
private val _event = MutableSharedFlow<ExpressTransactionsEvent>(
replay = 1,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val event: Flow<ExpressTransactionsEvent> = _event
override suspend fun send(event: ExpressTransactionsEvent) {
_event.emit(event)
}
}

View file

@ -45,7 +45,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
internal fun TokenDetailsScreen(
state: TokenDetailsState,
@ -62,6 +62,7 @@ internal fun TokenDetailsScreen(
) { scaffoldPaddings ->
val listState = rememberLazyListState()
val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle()
val dialogConfig = state.dialogConfig
val betweenItemsPadding = TangemTheme.dimens.spacing12
val horizontalPadding = TangemTheme.dimens.spacing16
val itemModifier = Modifier
@ -162,7 +163,9 @@ internal fun TokenDetailsScreen(
}
}
TokenDetailsDialogs(state = state)
if (dialogConfig != null) {
TokenDetailsDialogs(dialogConfig = dialogConfig)
}
state.bottomSheetConfig?.let { config ->
when (config.content) {

View file

@ -1,16 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.runtime.Composable
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
@Composable
internal fun TokenDetailsDialogs(state: TokenDetailsState) {
val dialogConfig = state.dialogConfig
if (dialogConfig != null && dialogConfig.isShow) {
internal fun TokenDetailsDialogs(dialogConfig: TokenDetailsDialogConfig) {
if (dialogConfig.isShow) {
TokenDetailsDialog(config = dialogConfig)
}
}
@ -21,7 +19,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) {
message = config.content.message.resolveReference(),
confirmButton = DialogButtonUM(
title = config.content.confirmButtonConfig.text.resolveReference(),
isWarning = config.content.confirmButtonConfig.warning,
isWarning = config.content.confirmButtonConfig.hasWarning,
onClick = config.content.confirmButtonConfig.onClick,
),
onDismissDialog = config.onDismissRequest,
@ -29,7 +27,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) {
dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig ->
DialogButtonUM(
title = cancelButtonConfig.text.resolveReference(),
isWarning = cancelButtonConfig.warning,
isWarning = cancelButtonConfig.hasWarning,
onClick = cancelButtonConfig.onClick,
)
},