Updated on 2026-08-14
This commit is contained in:
parent
c1ad1e43ce
commit
68fbe17731
18 changed files with 968 additions and 29 deletions
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.common.ui.footers
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Sending info text with display animation.
|
||||
* Must show text only when keyboard is closed
|
||||
*
|
||||
* @param footerText text to display
|
||||
* @param modifier composable modifier
|
||||
*/
|
||||
@Composable
|
||||
fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) {
|
||||
var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) }
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
// the text should appear when the keyboard is closed
|
||||
LaunchedEffect(footerText != TextReference.EMPTY, keyboard) {
|
||||
if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
isVisibleProxy = footerText != TextReference.EMPTY
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisibleProxy,
|
||||
modifier = modifier,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = fadeOut(tween(durationMillis = 300)),
|
||||
label = "Animate show sending state text",
|
||||
) {
|
||||
Text(
|
||||
text = footerText.resolveAnnotatedReference(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,4 +9,13 @@ fun interface ComposableContentComponent {
|
|||
|
||||
@Composable
|
||||
fun Content(modifier: Modifier)
|
||||
}
|
||||
|
||||
fun getEmptyComposableContentComponent() = EmptyComposableContentComponent
|
||||
|
||||
object EmptyComposableContentComponent : ComposableContentComponent {
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
fromDecimals = fromCryptoCurrency.decimals,
|
||||
toDecimals = toCryptoCurrency.decimals,
|
||||
fromAmount = fromAmount,
|
||||
providerId = expressProvider.name,
|
||||
providerId = expressProvider.providerId,
|
||||
rateType = rateType.name.lowercase(),
|
||||
requestId = requestId,
|
||||
refundAddress = refundData?.refundAddress,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class SwapTransactionSentUseCase(
|
|||
swapDataTransactionModel: SwapDataTransactionModel,
|
||||
provider: ExpressProvider,
|
||||
txHash: String,
|
||||
timestamp: Long,
|
||||
) = Either.catch {
|
||||
swapRepositoryV2.swapTransactionSent(
|
||||
userWallet = userWallet,
|
||||
|
|
@ -37,7 +38,6 @@ class SwapTransactionSentUseCase(
|
|||
txExtraId = swapDataTransactionModel.txExtraId,
|
||||
)
|
||||
if (provider.type.shouldStoreSwapTransaction()) {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
swapTransactionRepository.storeTransaction(
|
||||
userWalletId = userWallet.walletId,
|
||||
fromCryptoCurrency = fromCryptoCurrencyStatus.currency,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal fun NFTContent(stackState: ChildStack<NFTRoute, ComposableContentCompon
|
|||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.Companion.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Children(
|
||||
stack = stackState,
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ internal fun SendContent(
|
|||
stackState: ChildStack<CommonSendRoute, ComposableContentComponent>,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.Companion.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SendAppBar(navigationUM = navigationUM)
|
||||
Children(
|
||||
|
|
|
|||
|
|
@ -13,13 +13,21 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.express.models)
|
||||
implementation(projects.domain.swap.models)
|
||||
implementation(projects.domain.manageTokens.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.swap.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface SendWithSwapComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val callback: ModelCallback? = null,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendWithSwapComponent>
|
||||
|
||||
interface ModelCallback {
|
||||
fun onCloseSwap(lastAmount: String)
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,8 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.value.ObserveLifecycleMode
|
||||
import com.arkivanov.decompose.value.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.getEmptyComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.swap.models.R
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.ui.SendWithSwapContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
||||
@Assisted private val appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendWithSwapComponent.Params,
|
||||
private val sendDestinationComponentFactory: SendDestinationComponent.Factory,
|
||||
private val confirmComponentFactory: SendWithSwapConfirmComponent.Factory,
|
||||
) : SendWithSwapComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<SendWithSwapRoute>()
|
||||
|
||||
private val innerRouter = InnerRouter<SendWithSwapRoute>(
|
||||
stackNavigation = stackNavigation,
|
||||
popCallback = { onChildBack() },
|
||||
)
|
||||
|
||||
private val model: SendWithSwapModel = getOrCreateModel(params = params, router = innerRouter)
|
||||
|
||||
private val childStack = childStack(
|
||||
key = "sendWithSwapInnerStack",
|
||||
source = stackNavigation,
|
||||
serializer = null,
|
||||
initialConfiguration = model.initialRoute,
|
||||
handleBackButton = true,
|
||||
childFactory = { configuration, componentContext ->
|
||||
createChild(
|
||||
route = configuration,
|
||||
childContext = childByContext(
|
||||
componentContext = componentContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
init {
|
||||
childStack.subscribe(
|
||||
lifecycle = lifecycle,
|
||||
mode = ObserveLifecycleMode.CREATE_DESTROY,
|
||||
) { stack ->
|
||||
componentScope.launch {
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is SwapAmountComponent -> {
|
||||
// todo send with swap analytics
|
||||
activeComponent.updateState(model.uiState.value.amountUM)
|
||||
}
|
||||
is SendDestinationComponent -> {
|
||||
// todo send with swap analytics
|
||||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
is SendWithSwapConfirmComponent -> if (model.currentRoute.value.isEditMode) {
|
||||
// todo send with swap analytics
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
model.currentRoute.emit(stack.active.configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stackState by childStack.subscribeAsState()
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
SendWithSwapContent(navigationUM = state.navigationUM, stackState = stackState)
|
||||
}
|
||||
|
||||
private fun createChild(route: SendWithSwapRoute, childContext: AppComponentContext) = when (route) {
|
||||
is SendWithSwapRoute.Amount -> getAmountComponent(factoryContext = childContext)
|
||||
is SendWithSwapRoute.Destination -> getDestinationComponent(factoryContext = childContext)
|
||||
is SendWithSwapRoute.Confirm -> getConfirmComponent(factoryContext = childContext)
|
||||
is SendWithSwapRoute.Success -> getSuccessComponent(factoryContext = childContext)
|
||||
}
|
||||
|
||||
private fun getAmountComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
return SwapAmountComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SwapAmountComponentParams.AmountParams(
|
||||
amountUM = model.uiState.value.amountUM,
|
||||
title = resourceReference(R.string.common_send),
|
||||
currentRoute = model.currentRoute.filterIsInstance<SendWithSwapRoute.Amount>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = "",
|
||||
primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow,
|
||||
secondaryCryptoCurrency = null,
|
||||
swapDirection = SwapDirection.Direct,
|
||||
callback = model,
|
||||
userWallet = model.userWallet,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
val amountContentUM = model.uiState.value.amountUM as? SwapAmountUM.Content
|
||||
?: return getEmptyComposableContentComponent()
|
||||
val secondaryCryptoCurrency = amountContentUM.secondaryCryptoCurrencyStatus?.currency
|
||||
?: return getEmptyComposableContentComponent()
|
||||
|
||||
return sendDestinationComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = model.currentRoute.filterIsInstance<DestinationRoute>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = "",
|
||||
title = resourceReference(R.string.send_recipient_label),
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = secondaryCryptoCurrency,
|
||||
callback = model,
|
||||
onBackClick = ::onChildBack,
|
||||
onNextClick = model::onNextClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
return confirmComponentFactory.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendWithSwapConfirmComponent.Params(
|
||||
sendWithSwapUM = model.uiState.value,
|
||||
currentRoute = model.currentRoute.filterIsInstance<SendWithSwapRoute.Confirm>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
appCurrency = model.appCurrency,
|
||||
userWallet = model.userWallet,
|
||||
callback = model,
|
||||
analyticsCategoryName = "",
|
||||
primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow,
|
||||
primaryFeePaidCurrencyStatusFlow = model.primaryFeePaidCurrencyStatusFlow,
|
||||
swapDirection = SwapDirection.Direct,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
return SendWithSwapSuccessComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendWithSwapSuccessComponent.Params(
|
||||
sendWithSwapUMFlow = model.uiState,
|
||||
currentRoute = model.currentRoute.filterIsInstance<SendWithSwapRoute.Success>(),
|
||||
callback = model,
|
||||
analyticsCategoryName = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
val isEmptyStack = childStack.value.backStack.isEmpty()
|
||||
|
||||
if (isEmptyStack) {
|
||||
router.pop()
|
||||
} else {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendWithSwapComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendWithSwapComponent.Params,
|
||||
): DefaultSendWithSwapComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -22,15 +22,17 @@ import com.tangem.features.send.v2.api.subcomponents.destination.SendDestination
|
|||
import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui.SendWithSwapConfirmContent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
||||
@Assisted private val appComponentContext: AppComponentContext,
|
||||
|
|
@ -88,7 +90,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
)
|
||||
|
||||
private val sendNotificationsComponent = sendNotificationsComponentFactory.create(
|
||||
context = appComponentContext.childByContext(child("sendWithSwapConfirmNotifications")),
|
||||
context = appComponentContext.childByContext(child("sendWithSwapConfirmSendNotifications")),
|
||||
params = SendNotificationsComponent.Params(
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
|
|
@ -96,14 +98,23 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
notificationData = SendNotificationsComponent.Params.NotificationData(
|
||||
// todo fill with data
|
||||
destinationAddress = "",
|
||||
memo = "",
|
||||
amountValue = BigDecimal.ZERO,
|
||||
reduceAmountBy = BigDecimal.ZERO,
|
||||
isIgnoreReduce = false,
|
||||
fee = null,
|
||||
feeError = null,
|
||||
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
|
||||
memo = null,
|
||||
amountValue = model.confirmData.enteredAmount.orZero(),
|
||||
reduceAmountBy = model.confirmData.reduceAmountBy.orZero(),
|
||||
isIgnoreReduce = model.confirmData.isIgnoreReduce,
|
||||
fee = model.confirmData.fee,
|
||||
feeError = model.confirmData.feeError,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val swapNotificationsComponent = SwapNotificationsComponent(
|
||||
appComponentContext = appComponentContext.childByContext(child("sendWithSwapConfirmSwapNotifications")),
|
||||
params = SwapNotificationsComponent.Params(
|
||||
swapNotificationData = SwapNotificationsComponent.Params.SwapNotificationData(
|
||||
expressError = (model.confirmData.quote as? SwapQuoteUM.Error)?.expressError,
|
||||
fromCryptoCurrency = model.confirmData.fromCryptoCurrencyStatus?.currency,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -126,6 +137,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
override fun Content(modifier: Modifier) {
|
||||
val sendWithSwapUM by model.uiState.collectAsStateWithLifecycle()
|
||||
val sendNotificationsUM by sendNotificationsComponent.state.collectAsStateWithLifecycle()
|
||||
val swapNotificationsUM by swapNotificationsComponent.state.collectAsStateWithLifecycle()
|
||||
|
||||
SendWithSwapConfirmContent(
|
||||
sendWithSwapUM = sendWithSwapUM,
|
||||
|
|
@ -134,6 +146,8 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
sendNotificationsComponent = sendNotificationsComponent,
|
||||
sendNotificationsUM = sendNotificationsUM,
|
||||
swapNotificationsComponent = swapNotificationsComponent,
|
||||
swapNotificationsUM = swapNotificationsUM,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal data class ConfirmData(
|
||||
val enteredAmount: BigDecimal?,
|
||||
val reduceAmountBy: BigDecimal,
|
||||
val isIgnoreReduce: Boolean,
|
||||
val enteredDestination: String?,
|
||||
val fee: Fee?,
|
||||
val feeError: GetFeeError?,
|
||||
val fromCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
val toCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
val quote: SwapQuoteUM?,
|
||||
val rateType: ExpressRateType?,
|
||||
)
|
||||
|
|
@ -20,16 +20,21 @@ import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.common.SwapUtils.INCREASE_GAS_LIMIT_FOR_CEX
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateTrigger
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmInitialStateTransformer
|
||||
|
|
@ -51,7 +56,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase,
|
||||
private val estimateFeeUseCase: EstimateFeeUseCase,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val sendNotificationsUpdateTrigger: SendNotificationsUpdateTrigger,
|
||||
private val swapNotificationsUpdateTrigger: SwapNotificationsUpdateTrigger,
|
||||
private val sendNotificationsUpdateListener: SendNotificationsUpdateListener,
|
||||
private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener,
|
||||
swapTransactionSenderFactory: SwapTransactionSender.Factory,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), FeeSelectorModelCallback {
|
||||
|
||||
|
|
@ -61,20 +71,57 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
field = MutableStateFlow(params.sendWithSwapUM)
|
||||
|
||||
val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value
|
||||
val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus
|
||||
val primaryFeePaidCurrencyStatus: CryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value
|
||||
|
||||
private val amountUM = uiState.value.amountUM as? SwapAmountUM.Content
|
||||
|
||||
val primaryCurrency: CryptoCurrency = primaryCurrencyStatus.currency
|
||||
val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) {
|
||||
"Crypto currency must not be null"
|
||||
}
|
||||
|
||||
private val swapTransactionSender = swapTransactionSenderFactory.create(params.userWallet)
|
||||
|
||||
private var isAmountSubtractAvailable = false
|
||||
|
||||
private val amountUM
|
||||
get() = uiState.value.amountUM as? SwapAmountUM.Content
|
||||
private val destinationUM
|
||||
get() = uiState.value.destinationUM as? DestinationUM.Content
|
||||
private val feeSelectorUM
|
||||
get() = uiState.value.feeSelectorUM as? FeeSelectorUM.Content
|
||||
|
||||
val confirmData: ConfirmData
|
||||
get() {
|
||||
val amountUM = amountUM
|
||||
val fromAmount = amountUM?.swapDirection?.withSwapDirection(
|
||||
onDirect = { amountUM.primaryAmount },
|
||||
onReverse = { amountUM.secondaryAmount },
|
||||
)
|
||||
val amountState = fromAmount?.amountField as? AmountState.Data
|
||||
return ConfirmData(
|
||||
enteredAmount = amountState?.amountTextField?.cryptoAmount?.value,
|
||||
reduceAmountBy = amountState?.reduceAmountBy.orZero(),
|
||||
isIgnoreReduce = amountState?.isIgnoreReduce == true,
|
||||
enteredDestination = destinationUM?.addressTextField?.actualAddress,
|
||||
fee = feeSelectorUM?.selectedFeeItem?.fee,
|
||||
feeError = (uiState.value.feeSelectorUM as? FeeSelectorUM.Error)?.error,
|
||||
fromCryptoCurrencyStatus = amountUM?.swapDirection?.withSwapDirection(
|
||||
onDirect = { primaryCurrencyStatus },
|
||||
onReverse = { secondaryCurrencyStatus },
|
||||
),
|
||||
toCryptoCurrencyStatus = amountUM?.swapDirection?.withSwapDirection(
|
||||
onDirect = { secondaryCurrencyStatus },
|
||||
onReverse = { primaryCurrencyStatus },
|
||||
),
|
||||
quote = amountUM?.selectedQuote,
|
||||
rateType = amountUM?.swapRateType,
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
initAmountSubtractAvailability()
|
||||
configConfirmNavigation()
|
||||
initialState()
|
||||
subscribeOnNotificationUpdates()
|
||||
}
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
|
|
@ -127,10 +174,6 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
}
|
||||
ExpressProviderType.DEX,
|
||||
ExpressProviderType.DEX_BRIDGE,
|
||||
-> {
|
||||
// todo send with swap
|
||||
GetFeeError.UnknownError.left()
|
||||
}
|
||||
ExpressProviderType.ONRAMP,
|
||||
-> GetFeeError.DataError(
|
||||
cause = IllegalStateException("Provider $providerType is not supported in Send With Swap"),
|
||||
|
|
@ -139,7 +182,38 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onSendClick() {
|
||||
// todo swap send tx
|
||||
val provider = confirmData.quote?.provider ?: return
|
||||
modelScope.launch {
|
||||
swapTransactionSender.sendTransaction(
|
||||
confirmData = confirmData,
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
onExpressError = {
|
||||
// todo error
|
||||
},
|
||||
onSendError = {
|
||||
// todo error
|
||||
},
|
||||
onSendSuccess = { txHash, timestamp, data ->
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
networkId = primaryCurrencyStatus.currency.network.id,
|
||||
).getOrNull().orEmpty()
|
||||
|
||||
uiState.update {
|
||||
it.copy(
|
||||
confirmUM = ConfirmUM.Success(
|
||||
isPrimaryButtonEnabled = true,
|
||||
transactionDate = timestamp,
|
||||
txUrl = txUrl,
|
||||
provider = provider,
|
||||
swapDataModel = data,
|
||||
),
|
||||
)
|
||||
}
|
||||
router.replaceAll(SendWithSwapRoute.Success)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAmountSubtractAvailability() {
|
||||
|
|
@ -194,12 +268,38 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
feeError = feeSelectorUMError?.error,
|
||||
),
|
||||
)
|
||||
swapNotificationsUpdateTrigger.triggerUpdate(
|
||||
data = SwapNotificationData(
|
||||
expressError = (amountUM.selectedQuote as? SwapQuoteUM.Error)?.expressError,
|
||||
fromCryptoCurrency = amountUM.swapDirection.withSwapDirection(
|
||||
onDirect = { primaryCurrency },
|
||||
onReverse = { secondaryCurrency },
|
||||
),
|
||||
),
|
||||
)
|
||||
uiState.transformerUpdate(
|
||||
SendWithSwapConfirmationNotificationsTransformer(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnNotificationUpdates() {
|
||||
combine(
|
||||
flow = sendNotificationsUpdateListener.hasErrorFlow,
|
||||
flow2 = swapNotificationsUpdateListener.hasErrorFlow,
|
||||
) { hasSendError, hasSwapError ->
|
||||
val hasError = hasSendError || hasSwapError
|
||||
uiState.update {
|
||||
val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content
|
||||
it.copy(
|
||||
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
|
||||
isPrimaryButtonEnabled = !hasError && feeUM != null,
|
||||
) ?: it.confirmUM,
|
||||
)
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun configConfirmNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressProviderType
|
||||
import com.tangem.domain.swap.models.SwapDataModel
|
||||
import com.tangem.domain.swap.models.SwapDataTransactionModel
|
||||
import com.tangem.domain.swap.usecase.GetSwapDataUseCase
|
||||
import com.tangem.domain.swap.usecase.SwapTransactionSentUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapTransactionSender @AssistedInject constructor(
|
||||
private val getSwapDataUseCase: GetSwapDataUseCase,
|
||||
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
|
||||
private val swapTransactionSentUseCase: SwapTransactionSentUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
) {
|
||||
|
||||
suspend fun sendTransaction(
|
||||
confirmData: ConfirmData,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
onSendSuccess: (String, Long, SwapDataModel) -> Unit,
|
||||
onExpressError: (ExpressError) -> Unit,
|
||||
onSendError: (SendTransactionError?) -> Unit,
|
||||
) {
|
||||
val provider = confirmData.quote?.provider ?: return
|
||||
|
||||
when (val providerType = provider.type) {
|
||||
ExpressProviderType.CEX -> onCexTransaction(
|
||||
confirmData = confirmData,
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
onExpressError = onExpressError,
|
||||
onSendSuccess = onSendSuccess,
|
||||
onSendError = onSendError,
|
||||
)
|
||||
ExpressProviderType.DEX,
|
||||
ExpressProviderType.DEX_BRIDGE,
|
||||
ExpressProviderType.ONRAMP,
|
||||
-> {
|
||||
Timber.w("Provider $providerType is not supported in Send With Swap")
|
||||
onExpressError(ExpressError.UnknownError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onCexTransaction(
|
||||
confirmData: ConfirmData,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
onSendSuccess: (String, Long, SwapDataModel) -> Unit,
|
||||
onExpressError: (ExpressError) -> Unit,
|
||||
onSendError: (SendTransactionError?) -> Unit,
|
||||
) {
|
||||
val fromStatus = confirmData.fromCryptoCurrencyStatus ?: return
|
||||
val toStatus = confirmData.toCryptoCurrencyStatus ?: return
|
||||
val provider = confirmData.quote?.provider ?: return
|
||||
val rateType = confirmData.rateType ?: return
|
||||
val amountValue = confirmData.enteredAmount ?: return
|
||||
val feeValue = confirmData.fee?.amount?.value ?: return
|
||||
|
||||
val fromAmount = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = fromStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = confirmData.reduceAmountBy,
|
||||
)
|
||||
|
||||
val swapData = getSwapDataUseCase(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrencyStatus = fromStatus,
|
||||
fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals),
|
||||
toCryptoCurrencyStatus = toStatus,
|
||||
toAddress = confirmData.enteredDestination,
|
||||
expressProvider = provider,
|
||||
rateType = rateType,
|
||||
).getOrElse { onExpressError(it); return }
|
||||
|
||||
createAndSendCexTransaction(
|
||||
fromAmount = fromAmount,
|
||||
fromStatus = fromStatus,
|
||||
toStatus = toStatus,
|
||||
fee = confirmData.fee,
|
||||
provider = provider,
|
||||
swapData = swapData,
|
||||
onSendSuccess = onSendSuccess,
|
||||
onExpressError = onExpressError,
|
||||
onSendError = onSendError,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createAndSendCexTransaction(
|
||||
fromAmount: BigDecimal,
|
||||
fromStatus: CryptoCurrencyStatus,
|
||||
toStatus: CryptoCurrencyStatus,
|
||||
fee: Fee,
|
||||
provider: ExpressProvider,
|
||||
swapData: SwapDataModel,
|
||||
onSendSuccess: (String, Long, SwapDataModel) -> Unit,
|
||||
onExpressError: (ExpressError) -> Unit,
|
||||
onSendError: (SendTransactionError?) -> Unit,
|
||||
) {
|
||||
val swapTransaction = if (swapData.transaction is SwapDataTransactionModel.CEX) {
|
||||
swapData.transaction
|
||||
} else {
|
||||
onExpressError(ExpressError.UnknownError)
|
||||
return
|
||||
}
|
||||
|
||||
val txData = createTransferTransactionUseCase(
|
||||
amount = fromAmount.convertToSdkAmount(fromStatus.currency),
|
||||
fee = fee,
|
||||
memo = swapTransaction.txExtraId,
|
||||
destination = swapTransaction.txTo,
|
||||
userWalletId = userWallet.walletId,
|
||||
network = fromStatus.currency.network,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to create swap CEX tx data")
|
||||
onSendError(SendTransactionError.UnknownError(Exception(it)))
|
||||
return
|
||||
}
|
||||
|
||||
if (txData.extras == null && swapTransaction.txExtraId != null) {
|
||||
onExpressError(ExpressError.UnknownError)
|
||||
return
|
||||
}
|
||||
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = fromStatus.currency.network,
|
||||
).fold(
|
||||
ifLeft = { onSendError(it) },
|
||||
ifRight = { txHash ->
|
||||
val timestamp = System.currentTimeMillis()
|
||||
swapTransactionSentUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrencyStatus = fromStatus,
|
||||
toCryptoCurrencyStatus = toStatus,
|
||||
swapDataTransactionModel = swapTransaction,
|
||||
provider = provider,
|
||||
txHash = txHash,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
onSendSuccess(txHash, timestamp, swapData)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.toStringWithRightOffset(decimals: Int): String {
|
||||
return movePointRight(decimals).toPlainString()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(userWallet: UserWallet): SwapTransactionSender
|
||||
}
|
||||
}
|
||||
|
|
@ -10,13 +10,17 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.notifications.notifications
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -29,6 +33,8 @@ internal fun SendWithSwapConfirmContent(
|
|||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
sendNotificationsComponent: SendNotificationsComponent,
|
||||
sendNotificationsUM: ImmutableList<NotificationUM>,
|
||||
swapNotificationsComponent: SwapNotificationsComponent,
|
||||
swapNotificationsUM: ImmutableList<NotificationUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Content
|
||||
|
|
@ -53,20 +59,25 @@ internal fun SendWithSwapConfirmContent(
|
|||
}
|
||||
if (confirmUM != null) {
|
||||
// tapHelp(isDisplay = confirmUM.showTapHelp) // todo
|
||||
with(swapNotificationsComponent) {
|
||||
content(
|
||||
state = swapNotificationsUM,
|
||||
isClickDisabled = confirmUM.isTransactionInProcess,
|
||||
)
|
||||
}
|
||||
with(sendNotificationsComponent) {
|
||||
content(
|
||||
state = sendNotificationsUM,
|
||||
isClickDisabled = confirmUM.isTransactionInProcess,
|
||||
)
|
||||
}
|
||||
// notifications(
|
||||
// notifications = confirmUM.notifications,
|
||||
// isClickDisabled = confirmUM.isTransactionInProcess,
|
||||
// )
|
||||
notifications(
|
||||
notifications = confirmUM.notifications,
|
||||
isClickDisabled = confirmUM.isTransactionInProcess,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerHMax()
|
||||
// todo
|
||||
// SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY)
|
||||
SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.DefaultSendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface SendWithSwapModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendWithSwapModel::class)
|
||||
fun provideSendWithSwapModel(impl: SendWithSwapModel): Model
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SendWithSwapModuleBinds {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideSendWithSwapComponentFactory(impl: DefaultSendWithSwapComponent.Factory): SendWithSwapComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.model
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
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.decompose.navigation.Router
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class SendWithSwapModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(),
|
||||
SwapAmountComponent.ModelCallback,
|
||||
SendDestinationComponent.ModelCallback,
|
||||
SendWithSwapConfirmComponent.ModelCallback {
|
||||
|
||||
private val params: SendWithSwapComponent.Params = paramsContainer.require()
|
||||
|
||||
val initialRoute = SendWithSwapRoute.Amount(false)
|
||||
val currentRoute = MutableStateFlow<SendWithSwapRoute>(initialRoute)
|
||||
|
||||
var userWallet: UserWallet by Delegates.notNull()
|
||||
var appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
||||
val uiState: StateFlow<SendWithSwapUM>
|
||||
field = MutableStateFlow(initialState())
|
||||
|
||||
val isBalanceHiddenFlow: StateFlow<Boolean>
|
||||
field = MutableStateFlow(false)
|
||||
|
||||
val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
|
||||
field = MutableStateFlow(
|
||||
CryptoCurrencyStatus(
|
||||
currency = params.currency,
|
||||
value = CryptoCurrencyStatus.Loading,
|
||||
),
|
||||
)
|
||||
|
||||
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
|
||||
field = MutableStateFlow(
|
||||
CryptoCurrencyStatus(
|
||||
currency = params.currency,
|
||||
value = CryptoCurrencyStatus.Loading,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
initUserWallet()
|
||||
initAppCurrency()
|
||||
subscribeOnBalanceHidden()
|
||||
}
|
||||
|
||||
override fun onAmountResult(amountUM: SwapAmountUM) {
|
||||
uiState.update { it.copy(amountUM = amountUM) }
|
||||
}
|
||||
|
||||
override fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onResult(sendWithSwapUM: SendWithSwapUM) {
|
||||
uiState.value = sendWithSwapUM
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
}
|
||||
|
||||
override fun onSeparatorClick(lastAmount: String) {
|
||||
params.callback?.onCloseSwap(lastAmount)
|
||||
router.popTo(initialRoute)
|
||||
}
|
||||
|
||||
override fun onBackClick() = router.pop()
|
||||
|
||||
override fun onNextClick() {
|
||||
if (currentRoute.value.isEditMode) {
|
||||
onBackClick()
|
||||
} else {
|
||||
when (currentRoute.value) {
|
||||
is SendWithSwapRoute.Amount -> router.push(SendWithSwapRoute.Destination(isEditMode = false))
|
||||
is SendWithSwapRoute.Destination -> router.push(SendWithSwapRoute.Confirm)
|
||||
SendWithSwapRoute.Confirm -> router.push(SendWithSwapRoute.Success)
|
||||
SendWithSwapRoute.Success -> onBackClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initUserWallet() {
|
||||
getUserWalletUseCase(params.userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
getPrimaryCurrencyStatusUpdates(params.currency)
|
||||
},
|
||||
ifLeft = {
|
||||
Timber.w(it.toString())
|
||||
// todo send with swap error
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState(): SendWithSwapUM {
|
||||
return SendWithSwapUM(
|
||||
amountUM = SwapAmountUM.Empty(swapDirection = SwapDirection.Direct),
|
||||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPrimaryCurrencyStatusUpdates(cryptoCurrency: CryptoCurrency) {
|
||||
val wallet = userWallet
|
||||
val isMultiCurrency = wallet.isMultiCurrency
|
||||
val isSingleWalletWithToken = wallet is UserWallet.Cold &&
|
||||
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
|
||||
|
||||
getCurrencyStatus(
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
isSingleWalletWithToken = isSingleWalletWithToken,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
).onEach { maybeCryptoCurrency ->
|
||||
maybeCryptoCurrency.fold(
|
||||
ifRight = { cryptoCurrencyStatus ->
|
||||
primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
|
||||
primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
).getOrNull() ?: cryptoCurrencyStatus
|
||||
},
|
||||
ifLeft = {
|
||||
// todo send with swap error
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun getCurrencyStatus(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
isSingleWalletWithToken: Boolean,
|
||||
isMultiCurrency: Boolean,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
return when {
|
||||
isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = true,
|
||||
)
|
||||
isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = false,
|
||||
)
|
||||
else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnBalanceHidden() {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { balanceHidingSettings ->
|
||||
isBalanceHiddenFlow.update { balanceHidingSettings.isBalanceHidden }
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.plus
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
|
||||
@Composable
|
||||
internal fun SendWithSwapContent(
|
||||
navigationUM: NavigationUM,
|
||||
stackState: ChildStack<SendWithSwapRoute, ComposableContentComponent>,
|
||||
) {
|
||||
val navigationUM = navigationUM as? NavigationUM.Content ?: return
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
text = navigationUM.title.resolveReference(),
|
||||
onBackClick = navigationUM.backIconClick,
|
||||
iconRes = navigationUM.additionalIconRes,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation { child ->
|
||||
when (child.configuration) {
|
||||
SendWithSwapRoute.Confirm -> fade()
|
||||
SendWithSwapRoute.Success -> slide(orientation = Orientation.Vertical) + fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
it.instance.Content(Modifier.weight(1f))
|
||||
}
|
||||
// TODO refactor [REDACTED_TASK_KEY]
|
||||
val primaryButton = navigationUM.primaryButton
|
||||
Row(modifier = Modifier.padding(16.dp)) {
|
||||
TangemButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = primaryButton.textReference.resolveReference(),
|
||||
icon = primaryButton.iconRes?.let {
|
||||
TangemButtonIconPosition.End(it)
|
||||
} ?: TangemButtonIconPosition.None,
|
||||
enabled = primaryButton.isEnabled,
|
||||
onClick = primaryButton.onClick,
|
||||
showProgress = false,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue