Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-28 14:59:35 +05:00
commit 7de53d6b0c
20 changed files with 257 additions and 43 deletions

View file

@ -431,6 +431,7 @@ internal class ChildFactory @Inject constructor(
params = ChooseManagedTokensComponent.Params(
userWalletId = route.userWalletId,
initialCurrency = route.initialCurrency,
selectedCurrency = route.selectedCurrency,
source = ChooseManagedTokensComponent.Source.valueOf(route.source.name),
),
componentFactory = chooseManagedTokensComponentFactory,

View file

@ -126,6 +126,7 @@ sealed class AppRoute(val path: String) : Route {
data class ChooseManagedTokens(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val source: Source,
) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") {
enum class Source {

View file

@ -10,6 +10,7 @@ interface ChooseManagedTokensComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val source: Source,
)

View file

@ -60,11 +60,16 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor(
params = SwapChooseTokenNetworkComponent.Params(
userWalletId = config.userWalletId,
initialCurrency = config.initialCurrency,
selectedCurrency = config.selectedCurrency,
token = config.token,
onDismiss = model.bottomSheetNavigation::dismiss,
onResult = { swapCurrencies, cryptoCurrency ->
componentScope.launch {
swapChooseTokenNetworkTrigger.trigger(swapCurrencies, cryptoCurrency)
swapChooseTokenNetworkTrigger.trigger(
swapCurrencies = swapCurrencies,
cryptoCurrency = cryptoCurrency,
shouldResetNavigation = params.selectedCurrency != null,
)
router.pop()
}
},

View file

@ -12,6 +12,7 @@ internal sealed class ChooseManageTokensBottomSheetConfig {
data class SwapTokensBottomSheetConfig(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val token: ManagedCryptoCurrency.Token,
) : ChooseManageTokensBottomSheetConfig()
}

View file

@ -55,6 +55,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig(
userWalletId = params.userWalletId,
initialCurrency = params.initialCurrency,
selectedCurrency = params.selectedCurrency,
token = token,
),
)

View file

@ -40,6 +40,7 @@ internal class SendEntryPointModel @Inject constructor(
AppRoute.ChooseManagedTokens(
userWalletId = params.userWalletId,
initialCurrency = params.cryptoCurrency,
selectedCurrency = null,
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
),
)

View file

@ -230,6 +230,23 @@ internal class SendModel @Inject constructor(
params.callback?.onConvertToAnotherToken(lastAmount = lastAmount)
}
override fun resetSendNavigation() {
uiState.update {
it.copy(
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
).transform(DestinationUM.Empty()),
feeUM = FeeUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
confirmData = null,
navigationUM = NavigationUM.Empty,
)
}
router.popTo(Amount(isEditMode = false))
}
override fun onError(error: GetUserWalletError) {
Timber.w(error.toString())
showAlertError()

View file

@ -41,6 +41,7 @@ internal class SendAmountComponent(
interface ModelCallback : NavigationModelCallback {
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
fun onConvertToAnotherToken(lastAmount: String)
fun resetSendNavigation()
fun onError(error: GetUserWalletError)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.send.v2.subcomponents.amount.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import javax.inject.Inject
@ModelScoped
internal class SendAmountAlertFactory @Inject constructor(
private val uiMessageSender: UiMessageSender,
) {
fun showResetSendingAlert(onConfirm: () -> Unit) {
// todo fix localization [REDACTED_TASK_KEY]
uiMessageSender.send(
DialogMessage(
title = stringReference("Confirm Convert"),
message = stringReference("Proceed with conversion? Previous data will be reset."),
firstActionBuilder = {
EventMessageAction(
title = stringReference("Confirm"),
onClick = onConfirm,
)
},
secondActionBuilder = {
EventMessageAction(
title = stringReference("Not Now"),
onClick = onDismissRequest,
)
},
dismissOnFirstAction = true,
),
)
}
}

View file

@ -65,6 +65,7 @@ internal class SendAmountModel @Inject constructor(
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val rampStateManager: RampStateManager,
private val sendAmountAlertFactory: SendAmountAlertFactory,
) : Model(), SendAmountClickIntents {
private val params: SendAmountComponentParams = paramsContainer.require()
@ -242,9 +243,21 @@ internal class SendAmountModel @Inject constructor(
}
override fun onConvertToAnotherToken() {
val amountParams = params as? SendAmountComponentParams.AmountParams ?: return
if (amountParams.currentRoute.value.isEditMode) {
sendAmountAlertFactory.showResetSendingAlert {
params.callback.resetSendNavigation()
confirmConvertToToken()
}
} else {
confirmConvertToToken()
}
}
private fun confirmConvertToToken() {
val amountParams = params as? SendAmountComponentParams.AmountParams ?: return
val amountFieldData = uiState.value as? AmountState.Data
val amountParams = params as? SendAmountComponentParams.AmountParams
amountParams?.callback?.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty())
amountParams.callback.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty())
}
private fun subscribeOnAmountReduceToTriggerUpdates() {

View file

@ -12,6 +12,7 @@ interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent {
data class Params(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val token: ManagedCryptoCurrency.Token,
val onDismiss: () -> Unit,
val onResult: (SwapCurrencies, CryptoCurrency) -> Unit,

View file

@ -9,12 +9,18 @@ import kotlinx.coroutines.flow.SharedFlow
*/
interface SwapChooseTokenNetworkTrigger {
suspend fun trigger(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency)
suspend fun trigger(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency, shouldResetNavigation: Boolean)
}
/**
* Listens to swap token selection
*/
interface SwapChooseTokenNetworkListener {
val swapChooseTokenNetworkResultFlow: SharedFlow<Pair<SwapCurrencies, CryptoCurrency>>
}
val swapChooseTokenNetworkResultFlow: SharedFlow<SwapChooseTokenTriggerData>
}
data class SwapChooseTokenTriggerData(
val swapCurrencies: SwapCurrencies,
val cryptoCurrency: CryptoCurrency,
val shouldResetNavigation: Boolean,
)

View file

@ -39,5 +39,6 @@ internal class SwapAmountComponent @AssistedInject constructor(
interface ModelCallback : NavigationModelCallback {
fun onAmountResult(amountUM: SwapAmountUM)
fun onSeparatorClick(lastAmount: String)
fun resetSendWithSwapNavigation(resetNavigation: Boolean)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
@ -61,4 +62,29 @@ internal class SwapAmountAlertFactory @Inject constructor(
),
)
}
fun showCloseSendWithSwapAlert(onConfirm: () -> Unit) {
// todo fix localization [REDACTED_TASK_KEY]
uiMessageSender.send(
DialogMessage(
title = stringReference("Confirm cancellation"),
message = stringReference(
"Are you sure you want to cancel the conversion? After changing, previous data will be reset.",
),
firstActionBuilder = {
EventMessageAction(
title = stringReference("Confirm"),
onClick = onConfirm,
)
},
secondActionBuilder = {
EventMessageAction(
title = stringReference("Not Now"),
onClick = onDismissRequest,
)
},
dismissOnFirstAction = true,
),
)
}
}

View file

@ -181,19 +181,40 @@ internal class SwapAmountModel @Inject constructor(
}
override fun onSelectTokenClick() {
appRouter.push(
AppRoute.ChooseManagedTokens(
userWalletId = userWallet.walletId,
initialCurrency = primaryCryptoCurrency,
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
),
)
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
modelScope.launch {
val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true
val selectedCurrency = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus?.currency
appRouter.push(
AppRoute.ChooseManagedTokens(
userWalletId = userWallet.walletId,
initialCurrency = primaryCryptoCurrency,
selectedCurrency = selectedCurrency.takeIf { isEditMode },
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
),
)
}
}
override fun onSeparatorClick() {
// todo handle reverse pair click with swap redesign
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
modelScope.launch {
if (amountParams.currentRoute.firstOrNull()?.isEditMode == true) {
swapAmountAlertFactory.showCloseSendWithSwapAlert {
params.callback.resetSendWithSwapNavigation(resetNavigation = true)
confirmSendWithSwapClose()
}
} else {
confirmSendWithSwapClose()
amountParams.callback.onAmountResult(uiState.value)
}
}
}
private fun confirmSendWithSwapClose() {
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data
val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return
val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
if (primaryCryptoCurrencyStatus != null) {
@ -209,8 +230,7 @@ internal class SwapAmountModel @Inject constructor(
)
}
callback.onSeparatorClick(lastAmount = amountFieldData?.amountTextField?.value.orEmpty())
callback.onAmountResult(uiState.value)
amountParams.callback.onSeparatorClick(lastAmount = amountFieldData?.amountTextField?.value.orEmpty())
}
private fun subscribeOnCryptoCurrencyStatusFlow() {
@ -302,10 +322,13 @@ internal class SwapAmountModel @Inject constructor(
private fun observeChooseSelectToken() {
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
.onEach { (swapCurrencies, currency) ->
.onEach { data ->
(params as? SwapAmountComponentParams.AmountParams)?.callback?.resetSendWithSwapNavigation(
data.shouldResetNavigation,
)
uiState.update { amountUM ->
if (amountUM is SwapAmountUM.Content) {
initPairs(swapCurrencies, currency)
initPairs(data.swapCurrencies, data.cryptoCurrency)
amountUM.copy(
isPrimaryButtonEnabled = false,
secondaryAmount = SwapAmountFieldUM.Loading(

View file

@ -4,6 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenTriggerData
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
@ -14,10 +15,20 @@ internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() :
SwapChooseTokenNetworkTrigger,
SwapChooseTokenNetworkListener {
override val swapChooseTokenNetworkResultFlow: SharedFlow<Pair<SwapCurrencies, CryptoCurrency>>
field = MutableSharedFlow<Pair<SwapCurrencies, CryptoCurrency>>()
override val swapChooseTokenNetworkResultFlow: SharedFlow<SwapChooseTokenTriggerData>
field = MutableSharedFlow<SwapChooseTokenTriggerData>()
override suspend fun trigger(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency) {
swapChooseTokenNetworkResultFlow.emit(swapCurrencies to cryptoCurrency)
override suspend fun trigger(
swapCurrencies: SwapCurrencies,
cryptoCurrency: CryptoCurrency,
shouldResetNavigation: Boolean,
) {
swapChooseTokenNetworkResultFlow.emit(
SwapChooseTokenTriggerData(
swapCurrencies = swapCurrencies,
cryptoCurrency = cryptoCurrency,
shouldResetNavigation = shouldResetNavigation,
),
)
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.features.swap.v2.impl.R
import javax.inject.Inject
@ModelScoped
internal class SwapChooseTokenAlertFactory @Inject constructor(
private val uiMessageSender: UiMessageSender,
) {
fun getGenericErrorState(onDismiss: () -> Unit) {
uiMessageSender.send(
DialogMessage(
title = resourceReference(id = R.string.common_error),
message = resourceReference(id = R.string.common_unknown_error),
onDismissRequest = onDismiss,
firstActionBuilder = { okAction() },
),
)
}
fun showChangeTokenAlert(onConfirm: () -> Unit, onDismiss: () -> Unit) {
// todo fix localization [REDACTED_TASK_KEY]
uiMessageSender.send(
DialogMessage(
title = stringReference("Changing token"),
message = stringReference(
"Are you sure you want to change the token? After changing, previous data will be reset.",
),
firstActionBuilder = {
EventMessageAction(
title = stringReference("Change"),
onClick = onConfirm,
)
},
secondActionBuilder = {
EventMessageAction(
title = stringReference("Cancel"),
onClick = onDismiss,
)
},
dismissOnFirstAction = true,
),
)
}
}

View file

@ -4,15 +4,13 @@ import arrow.core.getOrElse
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.ui.UiMessageSender
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage
@ -35,7 +33,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
private val getSwapSupportedPairsUseCase: GetSwapSupportedPairsUseCase,
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val uiMessageSender: UiMessageSender,
private val swapChooseTokenAlertFactory: SwapChooseTokenAlertFactory,
) : Model() {
private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require()
@ -63,7 +61,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
private fun initContent() {
val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse {
Timber.e("Failed to get user wallet: $it")
getGenericErrorState()
swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss)
return
}
@ -73,7 +71,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
userWalletId = params.userWalletId,
).getOrElse {
Timber.e("Failed to get crypto currency")
getGenericErrorState()
swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss)
return@launch
}
val pairs = getSwapSupportedPairsUseCase.invoke(
@ -95,7 +93,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
uiState.update(
SwapChooseContentStateTransformer(
pairs = pairs,
onNetworkClick = params.onResult,
onNetworkClick = ::onSwapTokenClick,
tokenName = params.token.name,
onDismiss = params.onDismiss,
),
@ -103,15 +101,18 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
}
}
private fun getGenericErrorState() {
uiMessageSender.send(
DialogMessage(
title = resourceReference(id = R.string.common_error),
message = resourceReference(id = R.string.common_unknown_error),
onDismissRequest = params.onDismiss,
firstActionBuilder = { okAction() },
),
)
private fun onSwapTokenClick(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency) {
val prevSelectedCurrency = params.selectedCurrency?.network
if (prevSelectedCurrency == null || cryptoCurrency.network == prevSelectedCurrency) {
params.onResult(swapCurrencies, cryptoCurrency)
} else {
swapChooseTokenAlertFactory.showChangeTokenAlert(
onConfirm = {
params.onResult(swapCurrencies, cryptoCurrency)
},
onDismiss = params.onDismiss,
)
}
}
private companion object {

View file

@ -13,13 +13,13 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
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.models.wallet.UserWallet
import com.tangem.domain.models.wallet.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
@ -27,8 +27,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
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.common.SwapAlertFactory
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
@ -113,6 +113,20 @@ internal class SendWithSwapModel @Inject constructor(
router.popTo(initialRoute)
}
override fun resetSendWithSwapNavigation(resetNavigation: Boolean) {
uiState.update {
it.copy(
destinationUM = DestinationUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
)
}
if (resetNavigation) {
router.popTo(SendWithSwapRoute.Amount(false))
}
}
override fun onBackClick() = router.pop()
override fun onNextClick() {