diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt index 870d30bc07..bd12e6323b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt @@ -7,6 +7,7 @@ import kotlinx.collections.immutable.PersistentList data class ExpressTransactionsBlockState( val transactions: PersistentList, + val transactionsToDisplay: PersistentList, val bottomSheetSlot: BottomSheetSlot?, val dialogSlot: DialogSlot?, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt index a06c8a9f4f..e8eeac64b5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt @@ -27,6 +27,7 @@ internal class EmptyExpressTransactionsComponent( private fun getInitialState(): ExpressTransactionsBlockState { return ExpressTransactionsBlockState( transactions = persistentListOf(), + transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, dialogSlot = null, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt index 23ead18dcf..ee4fe81f30 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt @@ -25,6 +25,7 @@ internal class PreviewEmptyExpressTransactionsComponent : ExpressTransactionsCom private fun getInitialState(): ExpressTransactionsBlockState { return ExpressTransactionsBlockState( transactions = persistentListOf(), + transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, dialogSlot = null, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index da34d0e786..11e52a71c4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -149,7 +149,7 @@ internal fun TangemPayDetailsScreen( ) with(expressTransactionsComponent) { expressTransactionsContent( - state = expressState.transactions, + state = expressState.transactionsToDisplay, modifier = modifier .padding(start = 16.dp, end = 16.dp, top = 12.dp) .fillMaxWidth(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt index 6d5210c3cf..5e7a0d7096 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt @@ -1,19 +1,14 @@ 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 @@ -26,17 +21,12 @@ 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.ExpressStateFactory 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 @@ -55,9 +45,7 @@ internal class ExpressTransactionsModel @Inject constructor( 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, @@ -79,19 +67,12 @@ internal class ExpressTransactionsModel @Inject constructor( private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) - private val currentStateProvider: Provider = Provider { internalUiState.value } + private val currentStateProvider: Provider = Provider { internalUiState.value } private val stateFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - TokenDetailsStateFactory( - appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + ExpressStateFactory( currentStateProvider = currentStateProvider, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - tokenDetailsClickIntents = EmptyTokenDetailsClickIntents(), expressTransactionsClickIntents = this, - networkHasDerivationUseCase = networkHasDerivationUseCase, - getUserWalletUseCase = getUserWalletUseCase, - userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } @@ -106,15 +87,8 @@ internal class ExpressTransactionsModel @Inject constructor( ) } - private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - + private val internalUiState = MutableStateFlow(stateFactory.getInitialState()) val uiState: StateFlow = internalUiState - .map(::mapInnerState) - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = mapInnerState(internalUiState.value), - ) init { subscribeOnExternalEvents() @@ -123,7 +97,7 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onExpressTransactionClick(txId: String) { - val expressTxState = internalUiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId } + val expressTxState = internalUiState.value.transactionsToDisplay.firstOrNull { it.info.txId == txId } ?: return internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) } @@ -145,7 +119,7 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onDisposeExpressStatus() { - val bottomSheetState = internalUiState.value.bottomSheetConfig?.content + val bottomSheetState = internalUiState.value.bottomSheetSlot?.config?.content if (bottomSheetState is ExpressStatusBottomSheetConfig) { modelScope.launch { expressStatusFactory.removeTransactionOnBottomSheetClosed( @@ -158,7 +132,7 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onDismissBottomSheet() { - when (val bsContent = internalUiState.value.bottomSheetConfig?.content) { + when (val bsContent = internalUiState.value.bottomSheetSlot?.config?.content) { is ExpressStatusBottomSheetConfig -> { modelScope.launch(dispatchers.main) { expressStatusFactory.removeTransactionOnBottomSheetClosed(bsContent.value) @@ -177,35 +151,6 @@ internal class ExpressTransactionsModel @Inject constructor( 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 -> @@ -232,10 +177,7 @@ internal class ExpressTransactionsModel @Inject constructor( } .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) - maybeCurrencyStatus.onRight { status -> - cryptoCurrencyStatus = status - } + maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status } } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -260,7 +202,7 @@ internal class ExpressTransactionsModel @Inject constructor( task = { try { Result.success( - expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs), + expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.transactions), ) } catch (exception: CancellationException) { throw exception diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index a137a93bf5..f06bee0ec8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -86,7 +86,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig 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.state.factory.express.TokenDetailsExpressStatusFactory import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter @@ -137,7 +137,7 @@ internal class TokenDetailsModel @Inject constructor( @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, paramsContainer: ParamsContainer, - expressStatusFactory: ExpressStatusFactory.Factory, + tokenDetailsExpressStatusFactory: TokenDetailsExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, private val appRouter: AppRouter, private val router: InnerTokenDetailsRouter, @@ -218,7 +218,7 @@ internal class TokenDetailsModel @Inject constructor( // endregion private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - expressStatusFactory.create( + tokenDetailsExpressStatusFactory.create( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, currentStateProvider = Provider { uiState.value }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt new file mode 100644 index 0000000000..e888745a6f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import androidx.compose.runtime.Composable +import com.tangem.common.ui.expressStatus.state.DialogSlot +import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState +import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs +import com.tangem.utils.Provider +import kotlinx.collections.immutable.persistentListOf + +internal class ExpressStateFactory( + private val currentStateProvider: Provider, + private val expressTransactionsClickIntents: ExpressTransactionsClickIntents, +) { + + fun getInitialState(): ExpressTransactionsBlockState { + return ExpressTransactionsBlockState( + transactions = persistentListOf(), + transactionsToDisplay = persistentListOf(), + bottomSheetSlot = null, + dialogSlot = null, + ) + } + + fun getStateWithClosedDialog(): ExpressTransactionsBlockState { + val state = currentStateProvider() + val slot = state.dialogSlot ?: return state + return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false))) + } + + fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { + val state = currentStateProvider() + val slot = state.bottomSheetSlot ?: return state + return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false))) + } + + fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState { + return currentStateProvider().copy( + dialogSlot = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = expressTransactionsClickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig( + onConfirmClick = { + expressTransactionsClickIntents.onDisposeExpressStatus() + expressTransactionsClickIntents.onDismissDialog() + }, + onCancelClick = expressTransactionsClickIntents::onDismissDialog, + ), + ).toDialogSlot(), + ) + } + + private fun TokenDetailsDialogConfig.toDialogSlot(): DialogSlot { + val contentLambda: @Composable () -> Unit = { + TokenDetailsDialogs(this) + } + return DialogSlot(config = this, content = contentLambda) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index d2b9ffaddb..778b4f2edf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.datasource.local.swap.SwapTransactionStatusStore @@ -19,7 +20,6 @@ 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.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 import com.tangem.utils.Provider @@ -46,7 +46,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, @Assisted private val clickIntents: ExpressTransactionsClickIntents, @Assisted private val appCurrencyProvider: Provider, - @Assisted private val currentStateProvider: Provider, + @Assisted private val currentStateProvider: Provider, @Assisted private val userWallet: UserWallet, @Assisted private val cryptoCurrency: CryptoCurrency, ) { @@ -81,7 +81,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) { val state = currentStateProvider() - val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val bottomSheetConfig = state.bottomSheetSlot?.config?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose @@ -259,7 +259,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( fun create( clickIntents: ExpressTransactionsClickIntents, appCurrencyProvider: Provider, - currentStateProvider: Provider, + currentStateProvider: Provider, userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): ExchangeStatusFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index bb0dbbd4c4..7d3b0b6008 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -1,7 +1,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import androidx.compose.runtime.Composable import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.BottomSheetSlot import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency @@ -13,9 +16,9 @@ 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.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 +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -32,7 +35,7 @@ import kotlinx.coroutines.withContext @Suppress("LongParameterList") internal class ExpressStatusFactory @AssistedInject constructor( - @Assisted private val currentStateProvider: Provider, + @Assisted private val currentStateProvider: Provider, @Assisted private val clickIntents: ExpressTransactionsClickIntents, @Assisted private val cryptoCurrency: CryptoCurrency, @Assisted appCurrencyProvider: Provider, @@ -93,9 +96,9 @@ internal class ExpressStatusFactory @AssistedInject constructor( fun getStateWithUpdatedExpressTxs( expressTxs: PersistentList, updateBalance: (CryptoCurrency) -> Unit, - ): TokenDetailsState { + ): ExpressTransactionsBlockState { val state = currentStateProvider() - val config = state.bottomSheetConfig + val config = state.bottomSheetSlot?.config val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId } if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { @@ -108,13 +111,14 @@ internal class ExpressStatusFactory @AssistedInject constructor( } }.toPersistentList() return state.copy( - expressTxs = expressTxs, - expressTxsToDisplay = expressTxsToDisplay, - bottomSheetConfig = currentTx?.let(::updateStateWithExpressStatusBottomSheet) ?: config, + transactions = expressTxs, + transactionsToDisplay = expressTxsToDisplay, + bottomSheetSlot = (currentTx?.let(::updateStateWithExpressStatusBottomSheet) ?: config) + ?.toBottomSheetSlot(), ) } - fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState { + fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): ExpressTransactionsBlockState { val analyticEvents = when (expressState) { is ExchangeUM -> listOfNotNull( TokenExchangeAnalyticsEvent.CexTxStatusOpened( @@ -139,19 +143,19 @@ internal class ExpressStatusFactory @AssistedInject constructor( analyticEvents.forEach { analyticsEventsHandler.send(it) } return currentStateProvider().copy( - bottomSheetConfig = TangemBottomSheetConfig( + bottomSheetSlot = TangemBottomSheetConfig( isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ExpressStatusBottomSheetConfig( value = expressState, ), - ), + ).toBottomSheetSlot(), ) } fun updateStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TangemBottomSheetConfig? { val state = currentStateProvider() - val bottomSheetConfig = state.bottomSheetConfig + val bottomSheetConfig = state.bottomSheetSlot?.config val currentConfig = bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return bottomSheetConfig maybeGetLongTimeExchangeNotificationShowEvent( @@ -201,13 +205,22 @@ internal class ExpressStatusFactory @AssistedInject constructor( } } + private fun TangemBottomSheetConfig.toBottomSheetSlot(): BottomSheetSlot { + val contentLambda: @Composable () -> Unit = { + when (this.content) { + is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = this) + } + } + return BottomSheetSlot(config = this, content = contentLambda) + } + @AssistedFactory interface Factory { @Suppress("LongParameterList") fun create( clickIntents: ExpressTransactionsClickIntents, appCurrencyProvider: Provider, - currentStateProvider: Provider, + currentStateProvider: Provider, userWallet: UserWallet, cryptoCurrency: CryptoCurrency, cryptoCurrencyStatusProvider: Provider, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index 4543119759..cbdf9089fa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.domain.appcurrency.model.AppCurrency @@ -16,7 +17,6 @@ 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.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 import dagger.assisted.Assisted @@ -34,7 +34,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - @Assisted private val currentStateProvider: Provider, + @Assisted private val currentStateProvider: Provider, @Assisted private val cryptoCurrencyStatusProvider: Provider, @Assisted private val appCurrencyProvider: Provider, @Assisted private val clickIntents: ExpressTransactionsClickIntents, @@ -70,7 +70,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean) { val state = currentStateProvider() - val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val bottomSheetConfig = state.bottomSheetSlot?.config?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return if (selectedTx.activeStatus.isAutoDisposable || isForceDispose) { @@ -154,7 +154,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( @AssistedFactory interface Factory { fun create( - currentStateProvider: Provider, + currentStateProvider: Provider, cryptoCurrencyStatusProvider: Provider, appCurrencyProvider: Provider, clickIntents: ExpressTransactionsClickIntents, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt new file mode 100644 index 0000000000..7c10c7d5d6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt @@ -0,0 +1,272 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.SwapTransactionStatusStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +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.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 +import com.tangem.utils.Provider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.map +import timber.log.Timber +import kotlin.coroutines.cancellation.CancellationException + +@Suppress("LongParameterList") +internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( + private val swapTransactionRepository: SwapTransactionRepository, + private val swapRepository: SwapRepository, + private val quotesRepository: QuotesRepository, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, + private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, + private val swapTransactionStatusStore: SwapTransactionStatusStore, + private val analyticsEventsHandler: AnalyticsEventHandler, + @Assisted private val clickIntents: ExpressTransactionsClickIntents, + @Assisted private val appCurrencyProvider: Provider, + @Assisted private val currentStateProvider: Provider, + @Assisted private val userWallet: UserWallet, + @Assisted private val cryptoCurrency: CryptoCurrency, +) { + + private val swapTransactionsStateConverter by lazy { + TokenDetailsSwapTransactionsStateConverter( + clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, + appCurrencyProvider = appCurrencyProvider, + analyticsEventsHandler = analyticsEventsHandler, + ) + } + + operator fun invoke(): Flow> { + return swapTransactionRepository.getTransactions( + userWallet = userWallet, + cryptoCurrencyId = cryptoCurrency.id, + ).conflate() + .map { savedTransactions -> + val quotes = savedTransactions + ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } + ?.toSet() + ?.getQuotesOrEmpty() + .orEmpty() + + getExchangeStatusState( + savedTransactions = savedTransactions, + quoteStatuses = quotes, + ) + } + } + + suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return + + val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose + if (shouldDispose) { + swapTransactionRepository.removeTransaction( + userWalletId = userWallet.walletId, + txId = selectedTx.info.txId, + ) + } + } + + suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM { + return if (swapTx.activeStatus?.isTerminal == true) { + swapTx + } else { + val statusModel = getExchangeStatus(swapTx.info.txId, swapTx.provider) + + if (statusModel != null) { + swapTransactionsStateConverter.updateTxStatus( + tx = swapTx, + statusModel = statusModel, + ) + } else { + swapTx + } + } + } + + private suspend fun getExchangeStatus(txId: String, provider: SwapProvider): ExchangeStatusModel? { + return swapRepository.getExchangeStatus(userWallet = userWallet, txId = txId) + .fold( + ifLeft = { null }, + ifRight = { statusModel -> + sendStatusUpdateAnalytics(statusModel, provider) + + val accountId = if (accountsFeatureToggles.isFeatureEnabled) { + getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + .map { it.account.accountId } + .getOrNull() + } else { + null + } + + val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) { + if (accountId != null) { + addRefundCurrencyIfNeededNew( + accountId = accountId, + status = statusModel, + type = provider.type, + ) + } else { + Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + null + } + } else { + addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type) + } + + swapTransactionRepository.storeTransactionState( + txId = txId, + status = statusModel, + accountWithCurrency = if (refundTokenCurrency != null) { + Pair(accountId, refundTokenCurrency) + } else { + null + }, + ) + statusModel.copy(refundCurrency = refundTokenCurrency) + }, + ) + } + + private suspend fun sendStatusUpdateAnalytics(statusModel: ExchangeStatusModel, provider: SwapProvider) { + val txId = statusModel.txId ?: return + val status = toAnalyticStatus(statusModel.status) ?: return + val savedStatus = swapTransactionStatusStore.getTransactionStatus(txId) + + if (savedStatus != status) { + analyticsEventsHandler.send( + TokenExchangeAnalyticsEvent.CexTxStatusChanged(cryptoCurrency.symbol, status.value, provider.name), + ) + swapTransactionStatusStore.setTransactionStatus(txId, status) + } + } + + /** + * For now do it only for dex-bridge provider + */ + private suspend fun addRefundCurrencyIfNeededLegacy( + status: ExchangeStatusModel?, + type: ExchangeProviderType, + ): CryptoCurrency? { + status ?: return null + if (type != ExchangeProviderType.DEX_BRIDGE) return null + val refundNetwork = status.refundNetwork + val refundContractAddress = status.refundContractAddress + if (refundNetwork != null && refundContractAddress != null) { + return addCryptoCurrenciesUseCase( + userWalletId = userWallet.walletId, + contractAddress = refundContractAddress, + networkId = refundNetwork, + ).getOrNull() + } + return null + } + + private suspend fun addRefundCurrencyIfNeededNew( + accountId: AccountId, + status: ExchangeStatusModel?, + type: ExchangeProviderType, + ): CryptoCurrency? { + status ?: return null + if (type != ExchangeProviderType.DEX_BRIDGE) return null + val refundNetwork = status.refundNetwork + val refundContractAddress = status.refundContractAddress + + if (refundNetwork == null || refundContractAddress == null) return null + + return manageCryptoCurrenciesUseCase.add( + accountId = accountId, + contractAddress = refundContractAddress, + networkId = refundNetwork, + ) + .onLeft(Timber::e) + .getOrNull() + } + + private fun getExchangeStatusState( + savedTransactions: List?, + quoteStatuses: Set, + ): PersistentList { + if (savedTransactions == null) { + return persistentListOf() + } + + return swapTransactionsStateConverter.convert( + savedTransactions = savedTransactions, + quoteStatuses = quoteStatuses, + ) + } + + private fun toAnalyticStatus(status: ExchangeStatus?): ExpressAnalyticsStatus? { + return when (status) { + ExchangeStatus.New, + ExchangeStatus.Waiting, + ExchangeStatus.Sending, + ExchangeStatus.Confirming, + ExchangeStatus.Exchanging, + -> ExpressAnalyticsStatus.InProgress + ExchangeStatus.WaitingTxHash -> ExpressAnalyticsStatus.WaitingTxHash + ExchangeStatus.Verifying -> ExpressAnalyticsStatus.KYC + ExchangeStatus.Failed -> ExpressAnalyticsStatus.Fail + ExchangeStatus.TxFailed -> ExpressAnalyticsStatus.FailTx + ExchangeStatus.Finished -> ExpressAnalyticsStatus.Done + ExchangeStatus.Refunded -> ExpressAnalyticsStatus.Refunded + ExchangeStatus.Cancelled -> ExpressAnalyticsStatus.Cancelled + ExchangeStatus.Unknown -> ExpressAnalyticsStatus.Unknown + else -> null + } + } + + private suspend fun Set.getQuotesOrEmpty(): Set { + val rawIds = mapNotNull { it.rawCurrencyId }.toSet() + + return try { + quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds).orEmpty() + } catch (exception: CancellationException) { + throw exception + } catch (ignore: Exception) { + emptySet() + } + } + + @AssistedFactory + interface Factory { + fun create( + clickIntents: ExpressTransactionsClickIntents, + appCurrencyProvider: Provider, + currentStateProvider: Provider, + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + ): TokenDetailsExchangeStatusFactory + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt new file mode 100644 index 0000000000..19387b7150 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt @@ -0,0 +1,216 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.appcurrency.model.AppCurrency +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.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.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 +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class TokenDetailsExpressStatusFactory @AssistedInject constructor( + @Assisted private val currentStateProvider: Provider, + @Assisted private val clickIntents: ExpressTransactionsClickIntents, + @Assisted private val cryptoCurrency: CryptoCurrency, + @Assisted appCurrencyProvider: Provider, + @Assisted userWallet: UserWallet, + @Assisted cryptoCurrencyStatusProvider: Provider, + private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventsHandler: AnalyticsEventHandler, + tokenDetailsOnrampStatusFactory: TokenDetailsOnrampStatusFactory.Factory, + tokenDetailsExchangeStatusFactory: TokenDetailsExchangeStatusFactory.Factory, +) { + + private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { + tokenDetailsExchangeStatusFactory.create( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + currentStateProvider = currentStateProvider, + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + ) + } + + private val onrampStatusFactory by lazy(LazyThreadSafetyMode.NONE) { + tokenDetailsOnrampStatusFactory.create( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, + userWallet = userWallet, + ) + } + + fun getExpressStatuses(): Flow> = combine( + flow = exchangeStatusFactory(), + flow2 = onrampStatusFactory(), + ) { maybeExchange, maybeOnramp -> + persistentListOf(maybeOnramp, maybeExchange) + .flatten() + .sortedByDescending { it.info.timestamp } + .toPersistentList() + } + + suspend fun getUpdatedExpressStatuses(expressTxs: PersistentList) = + withContext(dispatchers.io) { + expressTxs.map { tx -> + async { + when (tx) { + is ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx) + is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx) + else -> null + } + } + }.awaitAll() + .filterNotNull() + .toPersistentList() + } + + fun getStateWithUpdatedExpressTxs( + expressTxs: PersistentList, + updateBalance: (CryptoCurrency) -> Unit, + ): TokenDetailsState { + val state = currentStateProvider() + val config = state.bottomSheetConfig + val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig + val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId } + if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { + updateBalance(currentTx.toCryptoCurrency) + } + val expressTxsToDisplay = expressTxs.filterNot { txs -> + when (txs) { + is ExpressTransactionStateUM.OnrampUM -> txs.activeStatus.isHidden + else -> false + } + }.toPersistentList() + return state.copy( + expressTxs = expressTxs, + expressTxsToDisplay = expressTxsToDisplay, + bottomSheetConfig = currentTx?.let(::updateStateWithExpressStatusBottomSheet) ?: config, + ) + } + + fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState { + val analyticEvents = when (expressState) { + is ExchangeUM -> listOfNotNull( + TokenExchangeAnalyticsEvent.CexTxStatusOpened( + cryptoCurrency.symbol, + ), + maybeGetLongTimeExchangeNotificationShowEvent( + expressState = expressState, + currentStateNotification = null, + isBottomSheetShown = true, + ), + ) + is ExpressTransactionStateUM.OnrampUM -> listOf( + TokenOnrampAnalyticsEvent.OnrampStatusOpened( + tokenSymbol = cryptoCurrency.symbol, + provider = expressState.providerName, + fiatCurrency = expressState.fromCurrencyCode, + ), + ) + else -> return currentStateProvider() + } + + analyticEvents.forEach { analyticsEventsHandler.send(it) } + + return currentStateProvider().copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = clickIntents::onDismissBottomSheet, + content = ExpressStatusBottomSheetConfig( + value = expressState, + ), + ), + ) + } + + fun updateStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TangemBottomSheetConfig? { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig + val currentConfig = bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return bottomSheetConfig + + maybeGetLongTimeExchangeNotificationShowEvent( + expressState = expressState, + currentStateNotification = (currentConfig.value as? ExchangeUM)?.notification, + isBottomSheetShown = bottomSheetConfig.isShown, + )?.let { analyticsEventsHandler.send(it) } + + return bottomSheetConfig.copy( + content = if (currentConfig.value != expressState) { + ExpressStatusBottomSheetConfig(expressState) + } else { + currentConfig + }, + ) + } + + suspend fun removeTransactionOnBottomSheetClosed( + expressState: ExpressTransactionStateUM, + isForceDispose: Boolean = false, + ) { + when (expressState) { + is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceDispose) + is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed( + isForceDispose, + ) + } + } + + private fun maybeGetLongTimeExchangeNotificationShowEvent( + expressState: ExpressTransactionStateUM, + currentStateNotification: ExchangeStatusNotification?, + isBottomSheetShown: Boolean, + ): TokenScreenAnalyticsEvent? { + val newState = expressState as? ExchangeUM + val newStateNotification = newState?.notification + return if (currentStateNotification !is ExchangeStatusNotification.LongTimeExchange && + newStateNotification is ExchangeStatusNotification.LongTimeExchange && + isBottomSheetShown + ) { + TokenExchangeAnalyticsEvent.LongTimeTransaction( + token = cryptoCurrency.symbol, + provider = newState.provider.name, + ) + } else { + null + } + } + + @AssistedFactory + interface Factory { + @Suppress("LongParameterList") + fun create( + clickIntents: ExpressTransactionsClickIntents, + appCurrencyProvider: Provider, + currentStateProvider: Provider, + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + cryptoCurrencyStatusProvider: Provider, + ): TokenDetailsExpressStatusFactory + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt new file mode 100644 index 0000000000..802663f78a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt @@ -0,0 +1,165 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus +import com.tangem.domain.appcurrency.model.AppCurrency +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.onramp.GetOnrampStatusUseCase +import com.tangem.domain.onramp.GetOnrampTransactionsUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase +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.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 +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import timber.log.Timber + +@Suppress("LongParameterList") +internal class TokenDetailsOnrampStatusFactory @AssistedInject constructor( + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val getOnrampStatusUseCase: GetOnrampStatusUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + @Assisted private val currentStateProvider: Provider, + @Assisted private val cryptoCurrencyStatusProvider: Provider, + @Assisted private val appCurrencyProvider: Provider, + @Assisted private val clickIntents: ExpressTransactionsClickIntents, + @Assisted private val cryptoCurrency: CryptoCurrency, + @Assisted private val userWallet: UserWallet, +) { + + private val onrampTransactionStateConverter by lazy(LazyThreadSafetyMode.NONE) { + TokenDetailsOnrampTransactionStateConverter( + clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + analyticsEventHandler = analyticsEventHandler, + ) + } + + operator fun invoke(): Flow> { + return getOnrampTransactionsUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyId = cryptoCurrency.id, + ).map { maybeTransaction -> + maybeTransaction.fold( + ifRight = { onrampTxs -> + val transactions = onrampTransactionStateConverter.convertList(onrampTxs) + transactions.clearHiddenTerminal() + transactions + }, + ifLeft = { persistentListOf() }, + ) + } + } + + suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean) { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return + + if (selectedTx.activeStatus.isAutoDisposable || isForceDispose) { + onrampRemoveTransactionUseCase(txId = selectedTx.info.txId) + } + } + + suspend fun updateOnrmapTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM): ExpressTransactionStateUM.OnrampUM { + return if (onrampTx.activeStatus.isTerminal) { + onrampTx + } else { + getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( + ifLeft = { error -> + Timber.e("Couldn't update onramp status. $error") + onrampTx + }, + ifRight = { statusModel -> + sendStatusUpdateAnalytics(onrampTx, statusModel) + onrampTx.copy( + activeStatus = statusModel.status, + info = onrampTx.info.copy( + txExternalId = statusModel.externalTxId, + txExternalUrl = statusModel.externalTxUrl, + ), + ) + }, + ) + } + } + + private suspend fun List.clearHiddenTerminal() { + this.filter { it.activeStatus.isHidden && it.activeStatus.isTerminal } + .forEach { onrampRemoveTransactionUseCase(txId = it.info.txId) } + } + + private suspend fun sendStatusUpdateAnalytics( + onrampTx: ExpressTransactionStateUM.OnrampUM, + statusModel: OnrampStatus, + ) { + val txId = statusModel.txId + val status = toAnalyticStatus(statusModel.status) ?: return + + if (statusModel.status != onrampTx.activeStatus) { + analyticsEventHandler.send( + TokenOnrampAnalyticsEvent.OnrampStatusChanged( + tokenSymbol = cryptoCurrency.symbol, + status = status.name, + provider = onrampTx.providerName, + fiatCurrency = onrampTx.fromCurrencyCode, + ), + ) + onrampUpdateTransactionStatusUseCase( + txId = txId, + externalTxUrl = statusModel.externalTxUrl.orEmpty(), + externalTxId = statusModel.externalTxId.orEmpty(), + status = statusModel.status, + ) + } + } + + private fun toAnalyticStatus(status: OnrampStatus.Status?): ExpressAnalyticsStatus? { + return when (status) { + Expired, + Paused, + -> ExpressAnalyticsStatus.Cancelled + Created, + WaitingForPayment, + PaymentProcessing, + Paid, + Sending, + RefundInProgress, + -> ExpressAnalyticsStatus.InProgress + Verifying -> ExpressAnalyticsStatus.KYC + Failed -> ExpressAnalyticsStatus.Fail + Finished -> ExpressAnalyticsStatus.Done + Refunded -> ExpressAnalyticsStatus.Refunded + null -> null + } + } + + @AssistedFactory + interface Factory { + fun create( + currentStateProvider: Provider, + cryptoCurrencyStatusProvider: Provider, + appCurrencyProvider: Provider, + clickIntents: ExpressTransactionsClickIntents, + cryptoCurrency: CryptoCurrency, + userWallet: UserWallet, + ): TokenDetailsOnrampStatusFactory + } +} \ No newline at end of file