Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-01 12:50:30 +05:00
parent 84cf236d5f
commit c71641b451
77 changed files with 152 additions and 3065 deletions

View file

@ -142,9 +142,7 @@ abstract class BaseTestCase : TestCase(
with(featureTogglesManager as MutableFeatureTogglesManager) {
changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true)
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true)
changeToggle("SEND_VIA_SWAP_ENABLED", true)
changeToggle("SWAP_REDESIGN_ENABLED", true)
changeToggle("SEND_REDESIGN_ENABLED", true)
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true)
}
}

View file

@ -104,7 +104,6 @@ class AmountStateConverterV2(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val isRedesignEnabled: Boolean,
private val isBalanceHidden: Boolean,
) : Converter<AmountParameters, AmountState> {
@ -122,23 +121,16 @@ class AmountStateConverterV2(
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
return AmountState.Empty(
isRedesignEnabled = isRedesignEnabled,
)
return AmountState.Empty(isRedesignEnabled = true)
}
return AmountState.Data(
title = value.title,
availableBalance = if (isRedesignEnabled) {
combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
).orMaskWithStars(isBalanceHidden)
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
.orMaskWithStars(isBalanceHidden)
},
availableBalance = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
).orMaskWithStars(isBalanceHidden),
availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden),
availableBalanceFiat = if (isBalanceHidden) {
TextReference.EMPTY
@ -171,7 +163,7 @@ class AmountStateConverterV2(
),
isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0,
isRedesignEnabled = isRedesignEnabled,
isRedesignEnabled = true,
)
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.common.ui.amountScreen.converters.field
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.extensions.*
@ -18,13 +17,12 @@ import com.tangem.utils.transformer.Transformer
* @property cryptoCurrencyStatus current cryptocurrency status
* @property maxEnterAmount new maximum enter amount boundary
* @property appCurrency current app currency
* @property isRedesignEnabled whether redesign mode is enabled
* @property isBalanceHidden whether balance is hidden
*/
class AmountBoundaryUpdateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary,
private val appCurrency: AppCurrency,
private val isRedesignEnabled: Boolean,
private val isBalanceHidden: Boolean,
) : Transformer<AmountState> {
@ -34,15 +32,11 @@ class AmountBoundaryUpdateTransformer(
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val availableBalance = if (isRedesignEnabled) {
combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
)
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
}
val availableBalance = combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
)
return prevState.copy(
availableBalance = availableBalance.orMaskWithStars(isBalanceHidden),

View file

@ -23,26 +23,14 @@
"name": "USEDESK_ENABLED",
"version": "undefined"
},
{
"name": "SEND_VIA_SWAP_ENABLED",
"version": "5.28.0"
},
{
"name": "SWAP_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "SEND_REDESIGN_ENABLED",
"version": "5.28.0"
},
{
"name": "HOT_WALLET_ENABLED",
"version": "undefined"
},
{
"name": "NFT_SEND_REDESIGN_ENABLED",
"version": "5.28.0"
},
{
"name": "TANGEM_PAY_ENABLED",
"version": "undefined"

View file

@ -65,7 +65,6 @@ fun InputRowRecipient(
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
onQrCodeClick: () -> Unit,
isRedesignEnabled: Boolean,
modifier: Modifier = Modifier,
singleLine: Boolean = false,
error: TextReference? = null,
@ -132,25 +131,15 @@ fun InputRowRecipient(
modifier = Modifier
.align(CenterEnd),
) {
if (isRedesignEnabled) {
QrButton(
visible = value.isBlank(),
onQrCodeClick = onQrCodeClick,
)
}
QrButton(
visible = value.isBlank(),
onQrCodeClick = onQrCodeClick,
)
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
backgroundColorEnabled = if (isRedesignEnabled) {
TangemTheme.colors.button.secondary
} else {
TangemTheme.colors.button.primary
},
textColor = if (isRedesignEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary2
},
backgroundColorEnabled = TangemTheme.colors.button.secondary,
textColor = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
)
}
@ -266,7 +255,6 @@ private fun InputRowRecipientPreview(
onPasteClick = {},
onQrCodeClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
isRedesignEnabled = false,
resolvedAddress = value.resolvedAddress,
)
}

View file

@ -23,7 +23,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.utils.Provider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -42,7 +41,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
private val shareManager: ShareManager,
private val sendFeatureToggles: SendFeatureToggles,
) {
private val disabledActionsInDemoMode = buildSet {
@ -159,17 +157,10 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val route = if (sendFeatureToggles.isSendWithSwapEnabled) {
AppRoute.SendEntryPoint(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
} else {
AppRoute.Send(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
}
val route = AppRoute.SendEntryPoint(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
router.push(route)
}

View file

@ -1,8 +1,3 @@
package com.tangem.features.send.v2.api
interface SendFeatureToggles {
val isSendRedesignEnabled: Boolean
val isNFTSendRedesignEnabled: Boolean
val isSendWithSwapEnabled: Boolean
}
interface SendFeatureToggles

View file

@ -17,7 +17,6 @@ sealed class DestinationUM {
val networkName: String,
val isValidating: Boolean = false,
val isInitialized: Boolean = false,
val isRedesignEnabled: Boolean = false,
val isRecentHidden: Boolean,
) : DestinationUM()

View file

@ -1,15 +1,5 @@
package com.tangem.features.send.v2
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.send.v2.api.SendFeatureToggles
internal class DefaultSendFeatureToggles(
private val featureToggles: FeatureTogglesManager,
) : SendFeatureToggles {
override val isSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
override val isNFTSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("NFT_SEND_REDESIGN_ENABLED")
override val isSendWithSwapEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED")
}
internal class DefaultSendFeatureToggles : SendFeatureToggles

View file

@ -32,9 +32,4 @@ internal sealed class CommonSendRoute : Route {
data class Amount(
override val isEditMode: Boolean,
) : CommonSendRoute()
@Serializable
data object Fee : CommonSendRoute() {
override val isEditMode: Boolean = true
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel
import dagger.Binds
import dagger.Module
@ -26,11 +25,6 @@ internal interface CommonSendModelModule {
@ClassKey(SendDestinationModel::class)
fun provideSendDestinationModel(model: SendDestinationModel): Model
@Binds
@IntoMap
@ClassKey(SendFeeModel::class)
fun provideSendFeeModel(model: SendFeeModel): Model
@Binds
@IntoMap
@ClassKey(NotificationsModel::class)

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.v2.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.send.v2.DefaultSendFeatureToggles
import com.tangem.features.send.v2.api.*
import com.tangem.features.send.v2.entrypoint.DefaultSendEntryPointComponent
@ -19,8 +18,8 @@ import javax.inject.Singleton
internal object SendFeatureModule {
@Provides
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles {
return DefaultSendFeatureToggles(featureTogglesManager)
fun provideSendFeatureToggles(): SendFeatureToggles {
return DefaultSendFeatureToggles()
}
}

View file

@ -38,8 +38,6 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -118,14 +116,6 @@ internal class DefaultSendComponent @AssistedInject constructor(
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
),
)
}
}
model.currentRoute.emit(stack.active.configuration)
}
@ -153,7 +143,6 @@ internal class DefaultSendComponent @AssistedInject constructor(
CommonSendRoute.Empty -> getStubComponent()
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
is CommonSendRoute.Amount -> getAmountComponent(factoryContext)
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
is CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
is CommonSendRoute.ConfirmSuccess -> getConfirmSuccessComponent(factoryContext)
}
@ -188,44 +177,11 @@ internal class DefaultSendComponent @AssistedInject constructor(
cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow,
callback = model,
predefinedValues = model.predefinedValues,
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
analyticsSendSource = model.analyticsSendSource,
),
)
}
@Suppress("ComplexCondition")
private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val state = model.uiState.value
val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.actualAddress
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value
return if (sendAmount != null && destinationAddress != null) {
SendFeeComponent(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = model.currentRoute.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = model.analyticCategoryName,
userWallet = model.userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
appCurrency = model.appCurrency,
onLoadFee = model::loadFee,
sendAmount = sendAmount,
destinationAddress = destinationAddress,
callback = model,
analyticsSendSource = model.analyticsSendSource,
),
)
} else {
model.showAlertError()
getStubComponent()
}
}
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value

View file

@ -5,11 +5,11 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.api.entity.FeeNonce
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import javax.inject.Inject
@ModelScoped
@ -19,15 +19,14 @@ internal class SendAnalyticHelper @Inject constructor(
fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM) {
val destinationUM = sendUM.destinationUM as? DestinationUM.Content
val feeUM = sendUM.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM)
val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.toAnalyticType()
analyticsEventHandler.send(
SendAnalyticEvents.TransactionScreenOpened(
token = cryptoCurrency.symbol,
feeType = feeType,
blockchain = cryptoCurrency.network.name,
nonceNotEmpty = feeSelectorUM.nonce != null,
nonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce,
),
)
analyticsEventHandler.send(

View file

@ -29,8 +29,6 @@ import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
@ -38,7 +36,7 @@ import kotlinx.coroutines.flow.*
internal class SendConfirmComponent(
appComponentContext: AppComponentContext,
params: Params,
private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendConfirmModel = getOrCreateModel(params = params)
@ -70,7 +68,6 @@ internal class SendConfirmComponent(
appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
predefinedValues = params.predefinedValues,
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow,
@ -81,25 +78,6 @@ internal class SendConfirmComponent(
onClick = model::showEditAmount,
)
private val feeBlockComponent = SendFeeBlockComponent(
appComponentContext = child("sendConfirmFeeBlock"),
params = SendFeeComponentParams.FeeBlockParams(
state = model.uiState.value.feeUM,
analyticsCategoryName = params.analyticsCategoryName,
userWallet = params.userWallet,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
sendAmount = model.confirmData.enteredAmount.orZero(),
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
analyticsSendSource = params.analyticsSendSource,
onLoadFee = params.onLoadFee,
),
onResult = model::onFeeResult,
onClick = model::showEditFee,
)
private val feeSelectorBlockComponent = feeSelectorComponentFactory.create(
context = appComponentContext,
params = FeeSelectorParams.FeeSelectorBlockParams(
@ -146,7 +124,6 @@ internal class SendConfirmComponent(
fun updateState(state: SendUM) {
destinationBlockComponent.updateState(state.destinationUM)
amountBlockComponent.updateState(state.amountUM)
feeBlockComponent.updateState(state.feeUM)
feeSelectorBlockComponent.updateState(state.feeSelectorUM)
model.updateState(state)
}
@ -160,7 +137,6 @@ internal class SendConfirmComponent(
sendUM = state,
destinationBlockComponent = destinationBlockComponent,
amountBlockComponent = amountBlockComponent,
feeBlockComponent = feeBlockComponent,
feeSelectorBlockComponent = feeSelectorBlockComponent,
notificationsComponent = notificationsComponent,
notificationsUM = notificationState,

View file

@ -6,8 +6,6 @@ internal interface SendConfirmClickIntents {
fun showEditAmount()
fun showEditFee()
fun onSendClick()
fun onExploreClick()

View file

@ -20,7 +20,6 @@ import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
@ -47,6 +46,7 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
import com.tangem.features.send.v2.common.CommonSendRoute
@ -59,13 +59,6 @@ import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.model.transformers.*
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.extensions.stripZeroPlainString
@ -96,8 +89,6 @@ internal class SendConfirmModel @Inject constructor(
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener,
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger,
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
@ -107,7 +98,6 @@ internal class SendConfirmModel @Inject constructor(
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val sendAmountReduceTrigger: SendAmountReduceTrigger,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
@ -133,10 +123,6 @@ internal class SendConfirmModel @Inject constructor(
get() = uiState.value.amountUM as? AmountState.Data
private val destinationUM
get() = uiState.value.destinationUM as? DestinationUM.Content
private val feeUM
get() = uiState.value.feeUM as? FeeUM.Content
private val feeSelectorUM
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
private val feeUMV2
get() = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content
@ -147,16 +133,8 @@ internal class SendConfirmModel @Inject constructor(
reduceAmountBy = amountState?.reduceAmountBy.orZero(),
isIgnoreReduce = amountState?.isIgnoreReduce == true,
enteredDestination = destinationUM?.addressTextField?.actualAddress,
fee = if (uiState.value.isRedesignEnabled) {
feeUMV2?.selectedFeeItem?.fee
} else {
feeSelectorUM?.selectedFee
},
feeError = if (uiState.value.isRedesignEnabled) {
(uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Error)?.error
} else {
(feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
},
fee = feeUMV2?.selectedFeeItem?.fee,
feeError = (uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Error)?.error,
)
private var sendIdleTimer: Long = 0L
@ -181,12 +159,6 @@ internal class SendConfirmModel @Inject constructor(
updateConfirmNotifications()
}
fun onFeeResult(feeUM: FeeUM) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update { it.copy(feeUM = feeUM) }
updateConfirmNotifications()
}
fun onAmountResult(amountUM: AmountState) {
_uiState.update { it.copy(amountUM = amountUM) }
updateConfirmNotifications()
@ -199,16 +171,7 @@ internal class SendConfirmModel @Inject constructor(
override fun onFeeReload() {
modelScope.launch {
if (uiState.value.isRedesignEnabled) {
feeSelectorReloadTrigger.triggerUpdate()
} else {
feeReloadTrigger.triggerUpdate(
feeData = SendFeeData(
amount = confirmData.enteredAmount,
destinationAddress = confirmData.enteredDestination,
),
)
}
feeSelectorReloadTrigger.triggerUpdate()
}
}
@ -261,30 +224,13 @@ internal class SendConfirmModel @Inject constructor(
}
}
override fun showEditFee() {
modelScope.launch {
neverShowTapHelpUseCase()
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Fee,
),
)
router.push(CommonSendRoute.Fee)
}
}
override fun onSendClick() {
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
verifyAndSendTransaction()
} else {
modelScope.launch {
if (uiState.value.isRedesignEnabled) {
feeSelectorCheckReloadTrigger.triggerCheckUpdate()
} else {
sendFeeCheckReloadTrigger.triggerCheckUpdate()
}
feeSelectorCheckReloadTrigger.triggerCheckUpdate()
}
}
}
@ -376,22 +322,12 @@ internal class SendConfirmModel @Inject constructor(
notificationsUpdateListener.hasErrorFlow
.onEach { hasError ->
_uiState.update {
if (_uiState.value.isRedesignEnabled) {
val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
isPrimaryButtonEnabled = !hasError && feeUM != null,
) ?: it.confirmUM,
)
} else {
val feeUM = it.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
isPrimaryButtonEnabled = !hasError && feeSelectorUM != null,
) ?: it.confirmUM,
)
}
val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
isPrimaryButtonEnabled = !hasError && feeUM != null,
) ?: it.confirmUM,
)
}
}
.launchIn(modelScope)
@ -401,17 +337,8 @@ internal class SendConfirmModel @Inject constructor(
val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return
val destination = destinationUM?.addressTextField?.actualAddress ?: return
val memo = destinationUM?.memoTextField?.value
val isRedesignEnabled = uiState.value.isRedesignEnabled
val fee = if (isRedesignEnabled) {
feeUMV2?.selectedFeeItem?.fee
} else {
feeSelectorUM?.selectedFee
}
val nonce = if (isRedesignEnabled) {
(feeUMV2?.feeNonce as? FeeNonce.Nonce)?.nonce
} else {
feeSelectorUM?.nonce
}
val fee = feeUMV2?.selectedFeeItem?.fee
val nonce = (feeUMV2?.feeNonce as? FeeNonce.Nonce)?.nonce
val feeValue = fee?.amount?.value ?: return
val receivingAmount = checkAndCalculateSubtractedAmount(
@ -478,10 +405,8 @@ internal class SendConfirmModel @Inject constructor(
addTokenToWalletIfNeeded()
sendBalanceUpdater.scheduleUpdates()
sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
if (uiState.value.isRedesignEnabled) {
params.callback.onResult(uiState.value)
params.onSendTransaction()
}
params.callback.onResult(uiState.value)
params.onSendTransaction()
},
)
}
@ -515,15 +440,6 @@ internal class SendConfirmModel @Inject constructor(
}
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
sendIdleTimer = SystemClock.elapsedRealtime()
if (isFeeResultSuccess) {
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))
verifyAndSendTransaction()
} else {
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
}
}.launchIn(modelScope)
feeSelectorCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
sendIdleTimer = SystemClock.elapsedRealtime()
if (isFeeResultSuccess) {
@ -560,25 +476,14 @@ internal class SendConfirmModel @Inject constructor(
)
_uiState.update {
it.copy(
confirmUM = if (uiState.value.isRedesignEnabled) {
SendConfirmationNotificationsTransformerV2(
feeSelectorUM = uiState.value.feeSelectorUM,
amountUM = uiState.value.amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM)
} else {
SendConfirmationNotificationsTransformer(
feeUM = uiState.value.feeUM,
amountUM = uiState.value.amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM)
},
confirmUM = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = uiState.value.feeSelectorUM,
amountUM = uiState.value.amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM),
)
}
}
@ -593,31 +498,15 @@ internal class SendConfirmModel @Inject constructor(
).filter {
it.second is CommonSendRoute.Confirm
}.onEach { (state, _) ->
val amountUM = state.amountUM as? AmountState.Data
val confirmUM = state.confirmUM
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
title = if (state.isRedesignEnabled) {
resourceReference(id = R.string.common_send)
} else {
resourceReference(
id = R.string.send_summary_title,
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
)
},
subtitle = if (uiState.value.isRedesignEnabled) {
null
} else {
amountUM?.title
},
backIconRes = if (state.isRedesignEnabled) {
when (confirmUM) {
is ConfirmUM.Success -> R.drawable.ic_close_24
else -> R.drawable.ic_back_24
}
} else {
R.drawable.ic_close_24
title = resourceReference(id = R.string.common_send),
subtitle = null,
backIconRes = when (confirmUM) {
is ConfirmUM.Success -> R.drawable.ic_close_24
else -> R.drawable.ic_back_24
},
backIconClick = {
analyticsEventHandler.send(
@ -628,11 +517,7 @@ internal class SendConfirmModel @Inject constructor(
isValid = confirmUM.isPrimaryButtonEnabled,
),
)
if (state.isRedesignEnabled) {
router.pop()
} else {
appRouter.pop()
}
router.pop()
},
primaryButton = primaryButtonUM(),
prevButton = null,

View file

@ -1,121 +0,0 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.utils.formatFooterFiatFee
import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class SendConfirmationNotificationsTransformer(
private val feeUM: FeeUM,
private val amountUM: AmountState,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
private val appCurrency: AppCurrency,
private val analyticsCategoryName: String,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
val feeUM = feeUM as? FeeUM.Content ?: return prevState
return state.copy(
sendingFooter = getSendingFooterText(),
notifications = buildList {
addTooHighNotification(feeUM.feeSelectorUM)
addTooLowNotification(feeUM)
}.toPersistentList(),
)
}
private fun MutableList<NotificationUM>.addTooLowNotification(feeUM: FeeUM.Content) {
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return
val minimumValue = multipleFees.minimum.amount.value ?: return
val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) {
add(NotificationUM.Warning.FeeTooLow)
analyticsEventHandler.send(
CommonSendAnalyticEvents.NoticeTransactionDelays(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
}
}
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM) {
if (feeSelectorUM !is FeeSelectorUM.Content) return
val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM)
if (isFeeTooHigh) {
add(NotificationUM.Warning.TooHigh(diff))
}
}
private fun getSendingFooterText(): TextReference {
val feeUM = feeUM as? FeeUM.Content
val amountUM = amountUM as? AmountState.Data
val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee
if (fee == null || amountUM == null) return TextReference.EMPTY
val fiatAmountValue = amountUM.amountTextField.fiatAmount.value
val fiatFeeValue = feeUM.rate?.let { fee.amount.value?.multiply(it) }
val fiatSendingValue = if (feeUM.isFeeConvertibleToFiat) {
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
} else {
fiatAmountValue
}
val fiatSending = fiatSendingValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
val fiatFee = formatFooterFiatFee(
amount = fee.amount.copy(value = fiatFeeValue),
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
isFeeApproximate = feeUM.isFeeApproximate,
appCurrency = appCurrency,
)
return if (feeUM.isTronToken && fee is Fee.Tron) {
getTronTokenFeeSendingText(
fee = fee,
fiatFee = fiatFee,
fiatSending = stringReference(fiatSending),
)
} else {
resourceReference(
id = if (feeUM.isFeeConvertibleToFiat) {
R.string.send_summary_transaction_description
} else {
R.string.send_summary_transaction_description_no_fiat_fee
},
formatArgs = wrappedList(fiatSending, fiatFee),
)
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.v2.send.confirm.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -9,26 +8,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.tapHelp
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.notifications
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
import kotlinx.collections.immutable.ImmutableList
@ -41,7 +32,6 @@ internal fun SendConfirmContent(
sendUM: SendUM,
destinationBlockComponent: DefaultSendDestinationBlockComponent,
amountBlockComponent: SendAmountBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
feeSelectorBlockComponent: FeeSelectorBlockComponent,
notificationsComponent: DefaultSendNotificationsComponent,
notificationsUM: ImmutableList<NotificationUM>,
@ -57,10 +47,8 @@ internal fun SendConfirmContent(
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
blocks(
uiState = sendUM,
destinationBlockComponent = destinationBlockComponent,
amountBlockComponent = amountBlockComponent,
feeBlockComponent = feeBlockComponent,
feeSelectorBlockComponent = feeSelectorBlockComponent,
)
if (confirmUM != null) {
@ -84,49 +72,19 @@ internal fun SendConfirmContent(
}
private fun LazyListScope.blocks(
uiState: SendUM,
destinationBlockComponent: DefaultSendDestinationBlockComponent,
amountBlockComponent: SendAmountBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
feeSelectorBlockComponent: FeeSelectorBlockComponent,
) {
item(key = BLOCKS_KEY) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
if (uiState.isRedesignEnabled) {
amountBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
feeSelectorBlockComponent.Content(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
} else {
TransactionDoneTitleAnimated(uiState)
destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier)
feeBlockComponent.Content(modifier = Modifier)
}
amountBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
feeSelectorBlockComponent.Content(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
}
}
}
@Composable
internal fun TransactionDoneTitleAnimated(uiState: SendUM) {
AnimatedVisibility(
visible = uiState.confirmUM is ConfirmUM.Success,
modifier = Modifier.padding(vertical = 12.dp),
) {
val wrappedConfirmUM = remember(this) { uiState.confirmUM as ConfirmUM.Success }
TransactionDoneTitle(
title = resourceReference(R.string.sent_transaction_sent_title),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
wrappedConfirmUM.transactionDate.toTimeFormat(),
),
),
modifier = Modifier.padding(vertical = 12.dp),
)
}
}

View file

@ -39,7 +39,6 @@ import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
@ -57,8 +56,6 @@ import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -70,7 +67,6 @@ import kotlin.properties.Delegates
internal interface SendComponentCallback :
SendAmountComponent.ModelCallback,
SendFeeComponent.ModelCallback,
SendDestinationComponent.ModelCallback,
SendConfirmComponent.ModelCallback,
SendConfirmSuccessComponent.ModelCallback
@ -96,7 +92,6 @@ internal class SendModel @Inject constructor(
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
private val sendFeatureToggles: SendFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), SendComponentCallback {
@ -113,11 +108,7 @@ internal class SendModel @Inject constructor(
field = MutableStateFlow(false)
val initialRoute = if (params.amount == null) {
if (uiState.value.isRedesignEnabled) {
Amount(isEditMode = false)
} else {
Destination(isEditMode = false)
}
Amount(isEditMode = false)
} else {
Empty
}
@ -167,19 +158,13 @@ internal class SendModel @Inject constructor(
uiState.update { it.copy(amountUM = amountUM) }
}
override fun onFeeResult(feeUM: FeeUM) {
uiState.update { it.copy(feeUM = feeUM) }
}
override fun onResult(sendUM: SendUM) {
uiState.update { sendUM }
}
override fun onBackClick() {
val isRedesignEnabled = uiState.value.isRedesignEnabled
when (val route = currentRoute.value) {
is Amount -> if (isRedesignEnabled && !route.isEditMode) {
is Amount -> if (!route.isEditMode) {
analyticsEventHandler.send(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = analyticCategoryName,
@ -189,7 +174,7 @@ internal class SendModel @Inject constructor(
),
)
}
is Destination -> if (isRedesignEnabled && !route.isEditMode) {
is Destination -> if (!route.isEditMode) {
analyticsEventHandler.send(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = analyticCategoryName,
@ -206,21 +191,12 @@ internal class SendModel @Inject constructor(
}
override fun onNextClick() {
val isRedesignEnabled = uiState.value.isRedesignEnabled
if (currentRoute.value.isEditMode) {
onBackClick()
} else {
when (currentRoute.value) {
is Amount -> if (isRedesignEnabled) {
router.push(Destination(isEditMode = false))
} else {
router.push(Confirm)
}
is Destination -> if (isRedesignEnabled) {
router.push(Confirm)
} else {
router.push(Amount(isEditMode = false))
}
is Amount -> router.push(Destination(isEditMode = false))
is Destination -> router.push(Confirm)
Confirm -> router.push(ConfirmSuccess)
else -> onBackClick()
}
@ -242,9 +218,7 @@ internal class SendModel @Inject constructor(
it.copy(
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
).transform(DestinationUM.Empty()),
feeUM = FeeUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
confirmData = null,
@ -480,15 +454,12 @@ internal class SendModel @Inject constructor(
}
private fun initialState(): SendUM = SendUM(
amountUM = AmountState.Empty(isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled),
amountUM = AmountState.Empty(isRedesignEnabled = true),
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
).transform(DestinationUM.Empty()),
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
confirmData = null,
feeSelectorUM = FeeSelectorUM.Loading,
)

View file

@ -6,15 +6,12 @@ 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.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.confirm.model.ConfirmData
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal data class SendUM(
val amountUM: AmountState,
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val feeSelectorUM: FeeSelectorUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
val isRedesignEnabled: Boolean,
val confirmData: ConfirmData?,
)

View file

@ -21,7 +21,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.ui.SendContent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
@ -30,14 +29,11 @@ import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import java.math.BigDecimal
internal class DefaultNFTSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@ -102,14 +98,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = analyticsCategoryName,
source = analyticsSendSource,
),
)
}
}
model.currentRouteFlow.emit(stack.active.configuration)
}
@ -131,7 +119,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext)
else -> getStubComponent()
@ -153,32 +140,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
),
)
private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val state = model.uiState.value
val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.actualAddress
return if (destinationAddress != null) {
SendFeeComponent(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = analyticsCategoryName,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
appCurrency = model.appCurrency,
sendAmount = BigDecimal.ZERO,
onLoadFee = model::loadFee,
destinationAddress = destinationAddress,
callback = model,
analyticsSendSource = analyticsSendSource,
),
)
} else {
getStubComponent()
}
}
private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create(
appComponentContext = factoryContext,
params = NFTSendConfirmComponent.Params(

View file

@ -6,11 +6,11 @@ import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.Companion.NFT_SEND_CATEGORY
import com.tangem.features.send.v2.api.entity.FeeNonce
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import javax.inject.Inject
@ModelScoped
@ -20,15 +20,14 @@ internal class NFTSendAnalyticHelper @Inject constructor(
fun nftSendSuccessAnalytics(cryptoCurrency: CryptoCurrency, nftSendUM: NFTSendUM) {
val destinationUM = nftSendUM.destinationUM as? DestinationUM.Content
val feeUM = nftSendUM.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM)
val feeSelectorUM = nftSendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.toAnalyticType()
analyticsEventHandler.send(
NFTSendAnalyticEvents.TransactionScreenOpened(
token = cryptoCurrency.symbol,
feeType = feeType,
blockchain = cryptoCurrency.network.name,
nonceNotEmpty = feeSelectorUM.nonce != null,
nonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce,
),
)
analyticsEventHandler.send(

View file

@ -32,8 +32,6 @@ import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel
import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -68,25 +66,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
onClick = model::showEditDestination,
)
private val feeBlockComponent = SendFeeBlockComponent(
appComponentContext = child("NFTSendConfirmFeeBlock"),
params = SendFeeComponentParams.FeeBlockParams(
state = model.uiState.value.feeUM,
analyticsCategoryName = params.analyticsCategoryName,
userWallet = params.userWallet,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
sendAmount = BigDecimal.ZERO,
onLoadFee = params.onLoadFee,
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
analyticsSendSource = params.analyticsSendSource,
),
onResult = model::onFeeResult,
onClick = model::showEditFee,
)
private val feeSelectorBlockComponent = feeSelectorComponentFactory.create(
context = child("NFTSendConfirmFeeSelectorBlock"),
params = FeeSelectorParams.FeeSelectorBlockParams(
@ -143,7 +122,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
fun updateState(state: NFTSendUM) {
destinationBlockComponent.updateState(state.destinationUM)
feeBlockComponent.updateState(state.feeUM)
model.updateState(state)
}
@ -155,7 +133,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
NFTSendConfirmContent(
nftSendUM = state,
destinationBlockComponent = destinationBlockComponent,
feeBlockComponent = feeBlockComponent,
feeSelectorBlockComponent = feeSelectorBlockComponent,
nftDetailsBlockComponent = nftDetailsBlockComponent,
notificationsComponent = notificationsComponent,

View file

@ -4,8 +4,6 @@ internal interface NFTSendConfirmClickIntents {
fun showEditDestination()
fun showEditFee()
fun onSendClick()
fun onExploreClick()

View file

@ -36,6 +36,7 @@ import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
import com.tangem.features.send.v2.common.CommonSendRoute
@ -47,15 +48,8 @@ import com.tangem.features.send.v2.sendnft.analytics.NFTSendAnalyticHelper
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.utils.transformer.update
@ -83,8 +77,6 @@ internal class NFTSendConfirmModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
private val notificationsUpdateListener: SendNotificationsUpdateListener,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger,
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener,
private val alertFactory: SendConfirmAlertFactory,
@ -93,7 +85,7 @@ internal class NFTSendConfirmModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val nftSendAnalyticHelper: NFTSendAnalyticHelper,
private val nftSendSuccessTrigger: NFTSendSuccessTrigger,
private val sendFeeReloadTrigger: SendFeeReloadTrigger,
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback {
@ -114,25 +106,13 @@ internal class NFTSendConfirmModel @Inject constructor(
private val destinationUM
get() = uiState.value.destinationUM as? DestinationUM.Content
private val feeUM
get() = uiState.value.feeUM as? FeeUM.Content
private val feeSelectorUM
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
val confirmData: ConfirmData
get() = ConfirmData(
enteredDestination = destinationUM?.addressTextField?.actualAddress,
enteredMemo = destinationUM?.memoTextField?.value,
fee = if (uiState.value.isRedesignEnabled) {
(uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content)?.selectedFeeItem?.fee
} else {
feeSelectorUM?.selectedFee
},
feeError = if (uiState.value.isRedesignEnabled) {
(uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Error)?.error
} else {
(feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
},
fee = (uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content)?.selectedFeeItem?.fee,
feeError = (uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Error)?.error,
)
private var sendIdleTimer: Long = 0L
@ -150,12 +130,6 @@ internal class NFTSendConfirmModel @Inject constructor(
updateConfirmNotifications()
}
fun onFeeResult(feeUM: FeeUM) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update { it.copy(feeUM = feeUM) }
updateConfirmNotifications()
}
override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update { it.copy(feeSelectorUM = feeSelectorUM) }
@ -169,13 +143,7 @@ internal class NFTSendConfirmModel @Inject constructor(
override fun onFeeReload() {
modelScope.launch {
sendFeeReloadTrigger.triggerUpdate(
// For now transfer one nft asset at a time
feeData = SendFeeData(
amount = BigDecimal.ZERO,
destinationAddress = confirmData.enteredDestination,
),
)
feeSelectorReloadTrigger.triggerUpdate()
}
}
@ -192,30 +160,13 @@ internal class NFTSendConfirmModel @Inject constructor(
}
}
override fun showEditFee() {
modelScope.launch {
neverShowTapHelpUseCase()
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Fee,
),
)
router.push(CommonSendRoute.Fee)
}
}
override fun onSendClick() {
_uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true))
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
verifyAndSendTransaction()
} else {
modelScope.launch {
if (uiState.value.isRedesignEnabled) {
feeSelectorCheckReloadTrigger.triggerCheckUpdate()
} else {
sendFeeCheckReloadTrigger.triggerCheckUpdate()
}
feeSelectorCheckReloadTrigger.triggerCheckUpdate()
}
}
}
@ -257,11 +208,7 @@ internal class NFTSendConfirmModel @Inject constructor(
private fun initialState() {
val confirmUM = uiState.value.confirmUM
val isEmptyFee = if (uiState.value.isRedesignEnabled) {
uiState.value.feeSelectorUM !is FeeSelectorUMRedesigned.Content
} else {
uiState.value.feeUM is FeeUM.Empty
}
val isEmptyFee = uiState.value.feeSelectorUM !is FeeSelectorUMRedesigned.Content
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false }
@ -293,13 +240,7 @@ internal class NFTSendConfirmModel @Inject constructor(
notificationsUpdateListener.hasErrorFlow
.onEach { hasError ->
_uiState.update {
val isFeeNotNull = if (uiState.value.isRedesignEnabled) {
it.feeSelectorUM is FeeSelectorUMRedesigned.Content
} else {
val feeUM = it.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
feeSelectorUM != null
}
val isFeeNotNull = it.feeSelectorUM is FeeSelectorUMRedesigned.Content
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
@ -372,24 +313,13 @@ internal class NFTSendConfirmModel @Inject constructor(
updateTransactionStatus(txData)
sendBalanceUpdater.scheduleUpdates()
nftSendAnalyticHelper.nftSendSuccessAnalytics(cryptoCurrency, uiState.value)
if (uiState.value.isRedesignEnabled) {
params.callback.onResult(uiState.value)
params.onSendTransaction()
}
params.callback.onResult(uiState.value)
params.onSendTransaction()
},
)
}
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
sendIdleTimer = SystemClock.elapsedRealtime()
if (isFeeResultSuccess) {
_uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true))
verifyAndSendTransaction()
} else {
_uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false))
}
}.launchIn(modelScope)
feeSelectorCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
sendIdleTimer = SystemClock.elapsedRealtime()
if (isFeeResultSuccess) {
@ -425,23 +355,13 @@ internal class NFTSendConfirmModel @Inject constructor(
}
_uiState.update {
it.copy(
confirmUM = if (uiState.value.isRedesignEnabled) {
NFTSendConfirmationNotificationsTransformerV2(
feeSelectorUM = uiState.value.feeSelectorUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = params.appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
} else {
NFTSendConfirmationNotificationsTransformer(
feeUM = uiState.value.feeUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
analyticsCategoryName = analyticsCategoryName,
appCurrency = params.appCurrency,
)
}.transform(uiState.value.confirmUM),
confirmUM = NFTSendConfirmationNotificationsTransformerV2(
feeSelectorUM = uiState.value.feeSelectorUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = params.appCurrency,
analyticsCategoryName = analyticsCategoryName,
).transform(uiState.value.confirmUM),
)
}
}
@ -453,23 +373,14 @@ internal class NFTSendConfirmModel @Inject constructor(
transform = { state, route -> state to route },
).onEach { (state, _) ->
val confirmUM = state.confirmUM
val confirmUMContent = confirmUM as? ConfirmUM.Content
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
title = resourceReference(R.string.nft_send),
subtitle = if (uiState.value.isRedesignEnabled) {
null
} else {
confirmUMContent?.walletName
},
backIconRes = if (state.isRedesignEnabled) {
when (confirmUM) {
is ConfirmUM.Success -> R.drawable.ic_close_24
else -> R.drawable.ic_back_24
}
} else {
R.drawable.ic_close_24
subtitle = null,
backIconRes = when (confirmUM) {
is ConfirmUM.Success -> R.drawable.ic_close_24
else -> R.drawable.ic_back_24
},
backIconClick = {
analyticsEventHandler.send(
@ -480,11 +391,7 @@ internal class NFTSendConfirmModel @Inject constructor(
isValid = confirmUM.isPrimaryButtonEnabled,
),
)
if (state.isRedesignEnabled) {
router.pop()
} else {
appRouter.pop()
}
router.pop()
},
primaryButton = primaryButtonUM(),
prevButton = null,

View file

@ -1,103 +0,0 @@
package com.tangem.features.send.v2.sendnft.confirm.model.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.utils.formatFooterFiatFee
import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class NFTSendConfirmationNotificationsTransformer(
private val feeUM: FeeUM,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
private val analyticsCategoryName: String,
private val appCurrency: AppCurrency,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
val feeUM = feeUM as? FeeUM.Content ?: return prevState
return state.copy(
sendingFooter = getSendingFooterText(),
notifications = buildList {
addTooHighNotification(feeUM.feeSelectorUM)
addTooLowNotification(feeUM)
}.toPersistentList(),
)
}
private fun MutableList<NotificationUM>.addTooLowNotification(feeUM: FeeUM.Content) {
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return
val minimumValue = multipleFees.minimum.amount.value ?: return
val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) {
add(NotificationUM.Warning.FeeTooLow)
analyticsEventHandler.send(
CommonSendAnalyticEvents.NoticeTransactionDelays(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
}
}
private fun getSendingFooterText(): TextReference {
val feeUM = feeUM as? FeeUM.Content
val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee ?: return TextReference.EMPTY
val fiatFeeValue = feeUM.rate?.let { fee.amount.value?.multiply(it) }
val fiatFee = formatFooterFiatFee(
amount = fee.amount.copy(value = fiatFeeValue),
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
isFeeApproximate = feeUM.isFeeApproximate,
appCurrency = appCurrency,
)
return if (feeUM.isTronToken && fee is Fee.Tron) {
getTronTokenFeeSendingText(
fee = fee,
fiatFee = fiatFee,
fiatSending = resourceReference(R.string.common_nft),
)
} else {
resourceReference(
id = if (feeUM.isFeeConvertibleToFiat) {
R.string.send_summary_transaction_description
} else {
R.string.send_summary_transaction_description_no_fiat_fee
},
formatArgs = wrappedList(
resourceReference(R.string.common_nft),
fiatFee,
),
)
}
}
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM) {
if (feeSelectorUM !is FeeSelectorUM.Content) return
val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM)
if (isFeeTooHigh) {
add(NotificationUM.Warning.TooHigh(diff))
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.v2.sendnft.confirm.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -8,25 +7,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.tapHelp
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.notifications
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
import kotlinx.collections.immutable.ImmutableList
@ -39,7 +30,6 @@ internal fun NFTSendConfirmContent(
nftSendUM: NFTSendUM,
destinationBlockComponent: DefaultSendDestinationBlockComponent,
nftDetailsBlockComponent: NFTDetailsBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
feeSelectorBlockComponent: FeeSelectorBlockComponent,
notificationsComponent: DefaultSendNotificationsComponent,
notificationsUM: ImmutableList<NotificationUM>,
@ -50,10 +40,8 @@ internal fun NFTSendConfirmContent(
modifier = Modifier.padding(horizontal = 16.dp),
) {
blocks(
nftSendUM = nftSendUM,
destinationBlockComponent = destinationBlockComponent,
nftDetailsBlockComponent = nftDetailsBlockComponent,
feeBlockComponent = feeBlockComponent,
feeSelectorBlockComponent = feeSelectorBlockComponent,
)
if (confirmUM != null) {
@ -73,49 +61,19 @@ internal fun NFTSendConfirmContent(
}
private fun LazyListScope.blocks(
nftSendUM: NFTSendUM,
destinationBlockComponent: DefaultSendDestinationBlockComponent,
nftDetailsBlockComponent: NFTDetailsBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
feeSelectorBlockComponent: FeeSelectorBlockComponent,
) {
item(key = BLOCKS_KEY) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
if (nftSendUM.isRedesignEnabled) {
nftDetailsBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
feeSelectorBlockComponent.Content(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
} else {
TransactionDoneTitleAnimated(nftSendUM = nftSendUM)
destinationBlockComponent.Content(modifier = Modifier)
nftDetailsBlockComponent.Content(modifier = Modifier)
feeBlockComponent.Content(modifier = Modifier)
}
nftDetailsBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
feeSelectorBlockComponent.Content(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
}
}
}
@Composable
private fun TransactionDoneTitleAnimated(nftSendUM: NFTSendUM) {
AnimatedVisibility(
visible = nftSendUM.confirmUM is ConfirmUM.Success,
modifier = Modifier.padding(vertical = 12.dp),
) {
val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success }
TransactionDoneTitle(
title = resourceReference(R.string.sent_transaction_sent_title),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
wrappedConfirmUM.transactionDate.toTimeFormat(),
),
),
modifier = Modifier.padding(vertical = 12.dp),
)
}
}

View file

@ -32,7 +32,6 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
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
@ -43,8 +42,6 @@ import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -52,7 +49,6 @@ import javax.inject.Inject
import kotlin.properties.Delegates
internal interface SendNFTComponentCallback :
SendFeeComponent.ModelCallback,
SendDestinationComponent.ModelCallback,
NFTSendConfirmComponent.ModelCallback
@ -74,7 +70,6 @@ internal class NFTSendModel @Inject constructor(
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val alertFactory: SendConfirmAlertFactory,
private val sendFeatureToggles: SendFeatureToggles,
private val nftSendSuccessTrigger: NFTSendSuccessTrigger,
) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback {
@ -116,10 +111,6 @@ internal class NFTSendModel @Inject constructor(
uiState.update { it.copy(destinationUM = destinationUM) }
}
override fun onFeeResult(feeUM: FeeUM) {
uiState.update { it.copy(feeUM = feeUM) }
}
override fun onBackClick() {
if (currentRouteFlow.value == ConfirmSuccess) {
modelScope.launch {
@ -254,10 +245,8 @@ internal class NFTSendModel @Inject constructor(
private fun initialState(): NFTSendUM = NFTSendUM(
destinationUM = DestinationUM.Empty(),
feeUM = FeeUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
isRedesignEnabled = sendFeatureToggles.isNFTSendRedesignEnabled,
)
}

View file

@ -4,13 +4,10 @@ import com.tangem.common.ui.navigationButtons.NavigationUM
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.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal data class NFTSendUM(
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val feeSelectorUM: FeeSelectorUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
val isRedesignEnabled: Boolean,
)

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -38,21 +37,12 @@ internal class SendAmountBlockComponent(
val state by model.uiState.collectAsStateWithLifecycle()
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
if (params.isRedesignEnabled) {
AmountBlockV2(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
modifier = modifier,
)
} else {
AmountBlock(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
}
AmountBlockV2(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
modifier = modifier,
)
}
}

View file

@ -20,7 +20,6 @@ internal sealed class SendAmountComponentParams {
abstract val userWalletId: UserWalletId
abstract val appCurrency: AppCurrency
abstract val predefinedValues: PredefinedValues
abstract val isRedesignEnabled: Boolean
abstract val cryptoCurrency: CryptoCurrency
abstract val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
abstract val isBalanceHidingFlow: StateFlow<Boolean>
@ -31,7 +30,6 @@ internal sealed class SendAmountComponentParams {
override val userWalletId: UserWalletId,
override val appCurrency: AppCurrency,
override val predefinedValues: PredefinedValues,
override val isRedesignEnabled: Boolean,
override val cryptoCurrency: CryptoCurrency,
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
@ -46,7 +44,6 @@ internal sealed class SendAmountComponentParams {
override val appCurrency: AppCurrency,
override val userWalletId: UserWalletId,
override val predefinedValues: PredefinedValues,
override val isRedesignEnabled: Boolean,
override val cryptoCurrency: CryptoCurrency,
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,

View file

@ -16,7 +16,6 @@ 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.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -29,7 +28,6 @@ import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents.SelectedCurrencyType
@ -39,8 +37,6 @@ import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.isNullOrZero
@ -60,9 +56,7 @@ internal class SendAmountModel @Inject constructor(
private val sendAmountReduceListener: SendAmountReduceListener,
private val sendAmountUpdateListener: SendAmountUpdateListener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val rampStateManager: RampStateManager,
@ -129,7 +123,6 @@ internal class SendAmountModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
appCurrency = appCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
@ -166,7 +159,6 @@ internal class SendAmountModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
appCurrency = appCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
@ -186,7 +178,6 @@ internal class SendAmountModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
iconStateConverter = CryptoCurrencyToIconStateConverter(),
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isBalanceHidden = params.isBalanceHidingFlow.value,
).convert(
AmountParameters(
@ -312,14 +303,7 @@ internal class SendAmountModel @Inject constructor(
value = reduceTo,
),
)
val amountValue = (uiState.value as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
if (uiState.value.isRedesignEnabled) {
feeSelectorReloadTrigger.triggerUpdate()
} else {
feeReloadTrigger.triggerUpdate(
feeData = SendFeeData(amount = amountValue),
)
}
feeSelectorReloadTrigger.triggerUpdate()
}
.launchIn(modelScope)
}
@ -334,15 +318,7 @@ internal class SendAmountModel @Inject constructor(
value = reduceByData,
),
)
val amountValue = (uiState.value as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
if (uiState.value.isRedesignEnabled) {
feeSelectorReloadTrigger.triggerUpdate()
} else {
feeReloadTrigger.triggerUpdate(
feeData = SendFeeData(amount = amountValue),
)
}
feeSelectorReloadTrigger.triggerUpdate()
}
.launchIn(modelScope)
}
@ -384,7 +360,7 @@ internal class SendAmountModel @Inject constructor(
NavigationUM.Content(
title = resourceReference(R.string.send_amount_label),
subtitle = null,
backIconRes = if (route.isEditMode || params.isRedesignEnabled) {
backIconRes = if (route.isEditMode) {
R.drawable.ic_back_24
} else {
R.drawable.ic_close_24
@ -402,20 +378,6 @@ internal class SendAmountModel @Inject constructor(
params.callback.onNextClick()
},
),
prevButton = if (params.isRedesignEnabled) {
null
} else {
NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
isEnabled = true,
onClick = {
saveResult()
params.callback.onBackClick()
},
).takeIf { route.isEditMode.not() }
},
secondaryPairButtonsUM = null,
),
)
}.launchIn(modelScope)
@ -428,7 +390,7 @@ internal class SendAmountModel @Inject constructor(
// Allowed only on networks without tx extras (e.i. memo and destination tag)
val isExtrasSupported = cryptoCurrencyStatus.currency.network.transactionExtrasType.isTxExtrasSupported()
isSendWithSwapAvailable.update {
isAvailableForSwap && isMultiCurrency && sendFeatureToggles.isSendWithSwapEnabled && !isExtrasSupported
isAvailableForSwap && isMultiCurrency && !isExtrasSupported
}
}
}

View file

@ -44,7 +44,6 @@ internal class DefaultSendDestinationBlockComponent @AssistedInject constructor(
destinationUM = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
isRedesignEnabled = (params.state as? DestinationUM.Content)?.isRedesignEnabled ?: false,
onClick = onClick,
)
}

View file

@ -25,7 +25,6 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.api.entity.PredefinedValues
@ -66,7 +65,6 @@ internal class SendDestinationModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponentParams = paramsContainer.require()
@ -92,7 +90,6 @@ internal class SendDestinationModel @Inject constructor(
_uiState.update(
SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isInitialized = true,
),
)
@ -301,12 +298,11 @@ internal class SendDestinationModel @Inject constructor(
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, route) ->
val isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled
params.callback.onNavigationResult(
NavigationUM.Content(
title = params.title,
subtitle = null,
backIconRes = if (route.isEditMode || isRedesignEnabled) {
backIconRes = if (route.isEditMode) {
R.drawable.ic_back_24
} else {
R.drawable.ic_close_24
@ -325,16 +321,6 @@ internal class SendDestinationModel @Inject constructor(
}
params.callback.onBackClick()
},
additionalIconRes = if (isRedesignEnabled) {
null
} else {
R.drawable.ic_qrcode_scan_24
},
additionalIconClick = if (isRedesignEnabled) {
null
} else {
::onQrCodeScanClick
},
primaryButton = NavigationButton(
textReference = if (route.isEditMode) {
resourceReference(R.string.common_continue)

View file

@ -13,7 +13,6 @@ import com.tangem.utils.transformer.Transformer
internal class SendDestinationInitialStateTransformer(
val cryptoCurrency: CryptoCurrency,
val isRedesignEnabled: Boolean,
val isInitialized: Boolean = false,
) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM {
@ -59,7 +58,6 @@ internal class SendDestinationInitialStateTransformer(
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
networkName = cryptoCurrency.network.name,
isValidating = false,
isRedesignEnabled = isRedesignEnabled,
isRecentHidden = false,
)
}

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -17,7 +16,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -33,7 +31,6 @@ internal fun DestinationBlock(
destinationUM: DestinationUM,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
isRedesignEnabled: Boolean,
onClick: () -> Unit,
) {
if (destinationUM !is DestinationUM.Content) return
@ -46,72 +43,9 @@ internal fun DestinationBlock(
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing12),
) {
if (isRedesignEnabled) {
AddressWithMemoBlock(
address = destinationUM.addressTextField,
memo = destinationUM.memoTextField,
)
} else {
AddressBlock(destinationUM.addressTextField)
MemoBlock(destinationUM.memoTextField)
}
}
}
@Composable
private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) {
Text(
text = address.label.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
) {
IdentIcon(
address = address.value,
modifier = Modifier
.size(TangemTheme.dimens.size36)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.background(TangemTheme.colors.background.tertiary),
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
val blockchainAddress = address.briefBlockchainAddress
if (!blockchainAddress.isNullOrBlank()) {
Text(
text = blockchainAddress,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
@Composable
private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) {
if (memo != null && memo.value.isNotBlank()) {
HorizontalDivider(
color = TangemTheme.colors.icon.inactive,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
)
Text(
text = memo.label.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = memo.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
AddressWithMemoBlock(
address = destinationUM.addressTextField,
memo = destinationUM.memoTextField,
)
}
}
@ -176,7 +110,6 @@ private fun DestinationBlockPreview(
destinationUM = state,
isClickDisabled = false,
isEditingDisabled = false,
isRedesignEnabled = state.isRedesignEnabled,
onClick = {},
)
}
@ -210,7 +143,6 @@ private class DestinationBlockPreviewProvider : PreviewParameterProvider<Destina
networkName = "Ethereum",
isValidating = false,
isInitialized = true,
isRedesignEnabled = false,
isRecentHidden = false,
)
@ -223,13 +155,11 @@ private class DestinationBlockPreviewProvider : PreviewParameterProvider<Destina
blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
),
),
previewItem.copy(isRedesignEnabled = true),
previewItem.copy(
addressTextField = previewItem.addressTextField.copy(
value = "vitalik.eth",
blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
),
isRedesignEnabled = true,
),
)
}

View file

@ -62,12 +62,10 @@ internal fun SendDestinationContent(
isValidating = isValidating,
onAddressChange = clickIntents::onRecipientAddressValueChange,
onQrCodeClick = clickIntents::onQrCodeScanClick,
isRedesignEnabled = state.isRedesignEnabled,
)
memoField(
memoField = memoField,
onMemoChange = clickIntents::onRecipientMemoValueChange,
isRedesignEnabled = state.isRedesignEnabled,
)
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
@ -117,7 +115,6 @@ private fun LazyListScope.addressItem(
isValidating: Boolean,
onAddressChange: (String, EnterAddressSource) -> Unit,
onQrCodeClick: () -> Unit,
isRedesignEnabled: Boolean,
) {
item(key = ADDRESS_FIELD_KEY) {
FooterContainer(
@ -130,7 +127,6 @@ private fun LazyListScope.addressItem(
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) },
onQrCodeClick = onQrCodeClick,
isRedesignEnabled = isRedesignEnabled,
isError = isError,
isLoading = isValidating,
error = address.error,
@ -149,7 +145,6 @@ private fun LazyListScope.addressItem(
private fun LazyListScope.memoField(
memoField: DestinationTextFieldUM.RecipientMemo?,
onMemoChange: (String, Boolean) -> Unit,
isRedesignEnabled: Boolean,
) {
if (memoField != null) {
item(key = MEMO_FIELD_KEY) {
@ -158,24 +153,20 @@ private fun LazyListScope.memoField(
value = memoField.value,
label = memoField.label,
placeholder = placeholder,
footer = if (isRedesignEnabled) {
annotatedReference(
buildAnnotatedString {
append(stringResourceSafe(R.string.send_recipient_memo_footer_v2))
append("\n")
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
appendColored(
text = stringResourceSafe(
R.string.send_recipient_memo_footer_v2_highlighted,
),
color = TangemTheme.colors.text.secondary,
)
}
},
)
} else {
resourceReference(R.string.send_recipient_memo_footer)
},
footer = annotatedReference(
buildAnnotatedString {
append(stringResourceSafe(R.string.send_recipient_memo_footer_v2))
append("\n")
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
appendColored(
text = stringResourceSafe(
R.string.send_recipient_memo_footer_v2_highlighted,
),
color = TangemTheme.colors.text.secondary,
)
}
},
),
onValueChange = { onMemoChange(it, false) },
onPasteClick = { onMemoChange(it, true) },
modifier = Modifier.padding(top = 20.dp),
@ -184,7 +175,6 @@ private fun LazyListScope.memoField(
error = memoField.error,
isReadOnly = !memoField.isEnabled,
isValuePasted = memoField.isValuePasted,
isRedesignEnabled = isRedesignEnabled,
)
}
}

View file

@ -28,7 +28,6 @@ internal fun TextFieldWithPaste(
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
isRedesignEnabled: Boolean,
modifier: Modifier = Modifier,
footer: TextReference? = null,
labelStyle: TextStyle = TangemTheme.typography.body2,
@ -98,16 +97,8 @@ internal fun TextFieldWithPaste(
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
backgroundColorEnabled = if (isRedesignEnabled) {
TangemTheme.colors.button.secondary
} else {
TangemTheme.colors.button.primary
},
textColor = if (isRedesignEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary2
},
backgroundColorEnabled = TangemTheme.colors.button.secondary,
textColor = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
)

View file

@ -1,42 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.FeeBlock
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class SendFeeBlockComponent(
appComponentContext: AppComponentContext,
private val params: SendFeeComponentParams.FeeBlockParams,
val onResult: (FeeUM) -> Unit,
val onClick: () -> Unit,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendFeeModel = getOrCreateModel(params = params)
init {
model.uiState.onEach {
onResult(it)
}.launchIn(componentScope)
}
fun updateState(state: FeeUM) = model.updateState(state)
@Composable
override fun Content(modifier: Modifier) {
val state = model.uiState.collectAsStateWithLifecycle()
val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle()
FeeBlock(
feeUM = state.value,
isClickEnabled = isClickEnabled.value,
onClick = onClick,
)
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
import com.tangem.features.send.v2.subcomponents.fee.ui.SendFeeContent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal class SendFeeComponent(
appComponentContext: AppComponentContext,
params: SendFeeComponentParams.FeeParams,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendFeeModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state = model.uiState.collectAsStateWithLifecycle()
SendFeeContent(state = state.value, clickIntents = model)
}
interface ModelCallback : NavigationModelCallback {
fun onFeeResult(feeUM: FeeUM)
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee
import arrow.core.Either
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal
internal sealed class SendFeeComponentParams {
abstract val state: FeeUM
abstract val analyticsCategoryName: String
abstract val userWallet: UserWallet
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
abstract val appCurrency: AppCurrency
abstract val sendAmount: BigDecimal
abstract val destinationAddress: String
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
data class FeeParams(
override val state: FeeUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
override val appCurrency: AppCurrency,
override val sendAmount: BigDecimal,
override val destinationAddress: String,
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val currentRoute: Flow<CommonSendRoute.Fee>,
val callback: SendFeeComponent.ModelCallback,
) : SendFeeComponentParams()
data class FeeBlockParams(
override val state: FeeUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
override val appCurrency: AppCurrency,
override val sendAmount: BigDecimal,
override val destinationAddress: String,
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendFeeComponentParams()
}

View file

@ -1,8 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee
import java.math.BigDecimal
data class SendFeeData(
val amount: BigDecimal?,
val destinationAddress: String? = null,
)

View file

@ -1,66 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
interface SendFeeReloadTrigger {
/** Trigger fee update */
suspend fun triggerUpdate(feeData: SendFeeData)
}
interface SendFeeReloadListener {
/** Flow triggers fee reload */
val reloadTriggerFlow: Flow<SendFeeData>
}
interface SendFeeCheckReloadTrigger {
/** Trigger return callback with check result */
suspend fun callbackCheckResult(isSuccess: Boolean)
/** Trigger fee check reload */
suspend fun triggerCheckUpdate()
}
interface SendFeeCheckReloadListener {
/**
* Flow triggers fee check reload before transaction.
* Usually after significant time after fee was updated last time
*/
val checkReloadTriggerFlow: Flow<Unit>
/** Flow return result of fee check update */
val checkReloadResultFlow: Flow<Boolean>
}
@Singleton
internal class DefaultSendFeeReloadTrigger @Inject constructor() :
SendFeeReloadTrigger,
SendFeeReloadListener,
SendFeeCheckReloadTrigger,
SendFeeCheckReloadListener {
private val _reloadTriggerFlow = MutableSharedFlow<SendFeeData>()
override val reloadTriggerFlow = _reloadTriggerFlow.asSharedFlow()
private val _checkReloadTriggerFlow = MutableSharedFlow<Unit>()
override val checkReloadTriggerFlow = _checkReloadTriggerFlow.asSharedFlow()
private val _checkReloadResultFlow = MutableSharedFlow<Boolean>()
override val checkReloadResultFlow = _checkReloadResultFlow.asSharedFlow()
override suspend fun triggerUpdate(feeData: SendFeeData) {
_reloadTriggerFlow.emit(feeData)
}
override suspend fun triggerCheckUpdate() {
_checkReloadTriggerFlow.emit(Unit)
}
override suspend fun callbackCheckResult(isSuccess: Boolean) {
_checkReloadResultFlow.emit(isSuccess)
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.di
import com.tangem.features.send.v2.subcomponents.fee.*
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
internal interface SendFeeModule {
@Binds
@Singleton
fun provideSendFeeReloadTrigger(impl: DefaultSendFeeReloadTrigger): SendFeeReloadTrigger
@Binds
@Singleton
fun provideSendFeeReloadListener(impl: DefaultSendFeeReloadTrigger): SendFeeReloadListener
@Binds
@Singleton
fun provideSendFeeCheckReloadTrigger(impl: DefaultSendFeeReloadTrigger): SendFeeCheckReloadTrigger
@Binds
@Singleton
fun provideSendFeeCheckReloadListener(impl: DefaultSendFeeReloadTrigger): SendFeeCheckReloadListener
}

View file

@ -1,90 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
import java.math.RoundingMode
private val FEE_MAX_DIFF = BigDecimal("5")
/**
* Check and calculates subtracted amount
*/
internal fun checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
return if (isFeeCoverage) {
balance.minus(reduceAmountBy).minus(feeValue)
} else {
amountValue
}
}
/**
* Check if custom fee is too high
*/
internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorUM.Content): Pair<Boolean, String> {
val defaultResult = false to ""
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return defaultResult
val highValue = multipleFees.priority.amount.value ?: return defaultResult
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return defaultResult
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val diff = if (highValue > BigDecimal.ZERO) {
customValue / highValue
} else {
BigDecimal.ZERO
}
val isFeeTooHigh = feeSelectorState.selectedType == FeeType.Custom && diff > FEE_MAX_DIFF
return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP)
}
/**
* Check if custom fee is too low
*/
internal fun checkIfFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean {
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false
val minimumValue = multipleFees.minimum.amount.value ?: return false
val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return false
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
return feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue
}
/**
* Checks if sending amount with fee is greater than balance
*/
internal fun checkFeeCoverage(
isSubtractAvailable: Boolean,
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal?,
): Boolean {
if (!isSubtractAvailable) return false
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
}
/**
* Checks if fee exceeds fee paid currency balance
*/
fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean {
return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance
}

View file

@ -1,132 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model
import com.tangem.blockchain.common.transaction.TransactionFee
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.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import java.math.BigDecimal
import javax.inject.Inject
@ModelScoped
internal class SendFeeAlertFactory @Inject constructor(
private val messageSender: UiMessageSender,
) {
fun checkAndShowAlerts(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit) {
val showFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick)
val showFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick)
if (!showFeeTooLow && !showFeeTooHigh) {
onConfirmClick()
}
}
/**
* Check if custom fee is too low
*/
private fun checkAndShowFeeTooLow(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit): Boolean {
val isFeeTooLow = checkIfFeeTooLow(feeSelectorUM)
if (isFeeTooLow) {
messageSender.send(
DialogMessage(
message = resourceReference(id = R.string.send_alert_fee_too_low_text),
dismissOnFirstAction = true,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_continue),
onClick = onConfirmClick,
)
},
secondActionBuilder = { cancelAction() },
),
)
}
return isFeeTooLow
}
/**
* Check if custom fee is too high
*/
private fun checkAndShowFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit): Boolean {
val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM)
if (isFeeTooHigh) {
messageSender.send(
DialogMessage(
message = resourceReference(
id = R.string.send_alert_fee_too_high_text,
formatArgs = wrappedList(diff),
),
dismissOnFirstAction = true,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_continue),
onClick = onConfirmClick,
)
},
secondActionBuilder = { cancelAction() },
),
)
}
return isFeeTooHigh
}
fun getFeeUpdatedAlert(newFee: TransactionFee, feeUM: FeeUM, proceedAction: () -> Unit, stopAction: () -> Unit) {
if (feeUM !is FeeUM.Content) return
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val newFee = when (newFee) {
is TransactionFee.Single -> newFee.normal
is TransactionFee.Choosable -> {
when (feeSelectorUM.selectedType) {
FeeType.Slow -> newFee.minimum
FeeType.Market -> newFee.normal
FeeType.Fast -> newFee.priority
FeeType.Custom -> return
}
}
}
val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO
val oldFeeValue = feeSelectorUM.selectedFee?.amount?.value ?: BigDecimal.ZERO
if (newFeeValue > oldFeeValue) {
messageSender.send(
DialogMessage(
message = resourceReference(id = R.string.send_notification_high_fee_title),
dismissOnFirstAction = true,
onDismissRequest = { stopAction() },
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_understand),
onClick = { stopAction(); onDismissRequest() },
)
},
),
)
} else {
proceedAction()
}
}
fun getFeeUnreachableErrorState(onFeeReload: () -> Unit) {
messageSender.send(
DialogMessage(
title = resourceReference(R.string.send_fee_unreachable_error_title),
message = resourceReference(R.string.send_fee_unreachable_error_text),
dismissOnFirstAction = true,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.warning_button_refresh),
onClick = onFeeReload,
)
},
),
)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
internal interface SendFeeClickIntents {
fun feeReload()
fun onFeeSelectorClick(feeType: FeeType)
fun onCustomFeeValueChange(index: Int, value: String)
fun onNonceChange(value: String)
fun onReadMoreClick()
fun onNextClick()
}

View file

@ -1,295 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model
import androidx.compose.runtime.Stable
import com.tangem.blockchain.common.AmountType
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadListener
import com.tangem.features.send.v2.subcomponents.fee.model.transformers.*
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.TangemLinks
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class SendFeeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeeAlertFactory: SendFeeAlertFactory,
private val feeReloadListener: SendFeeReloadListener,
private val feeCheckReloadListener: SendFeeCheckReloadListener,
private val feeCheckReloadTrigger: SendFeeCheckReloadTrigger,
) : Model(), SendFeeClickIntents {
private val params: SendFeeComponentParams = paramsContainer.require()
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
private val analyticsCategoryName = params.analyticsCategoryName
private val appCurrency = params.appCurrency
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
private val feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus
private var feeJobHolder = JobHolder()
init {
configFeeNavigation()
subscribeOnFeeReloadTriggerUpdates()
subscribeOnFeeCheckReloadTriggerUpdates()
initialState()
feeReload()
}
fun updateState(state: FeeUM) {
_uiState.value = state
}
override fun feeReload() {
modelScope.launch {
val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content
if (isShowLoading) {
_uiState.update(SendFeeLoadingTransformer)
}
params.onLoadFee().fold(
ifRight = {
_uiState.update(
SendFeeLoadedTransformer(
fees = it,
clickIntents = this@SendFeeModel,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
isFeeApproximate = isFeeApproximate(it.normal.amount.type),
),
)
updateFeeNotifications()
},
ifLeft = { feeError ->
if (isShowLoading) {
_uiState.update(SendFeeFailedTransformer(feeError))
}
updateFeeNotifications()
},
)
}.saveIn(feeJobHolder)
}
override fun onFeeSelectorClick(feeType: FeeType) {
_uiState.update(
SendFeeSelectTransformer(
feeType = feeType,
clickIntents = this@SendFeeModel,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
),
)
updateFeeNotifications()
if (feeType == FeeType.Custom) {
analyticsEventHandler.send(
CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = analyticsCategoryName),
)
}
}
override fun onCustomFeeValueChange(index: Int, value: String) {
_uiState.update(
SendFeeCustomValueChangeTransformer(
index = index,
value = value,
clickIntents = this@SendFeeModel,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
),
)
updateFeeNotifications()
}
override fun onNonceChange(value: String) {
_uiState.update(
SendFeeNonceChangeTransformer(value = value),
)
}
override fun onReadMoreClick() {
urlOpener.openUrl(TangemLinks.FEE_BLOG_LINK)
}
override fun onNextClick() {
val feeUM = uiState.value as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
if (feeSelectorUM != null) {
sendFeeAlertFactory.checkAndShowAlerts(feeSelectorUM) {
val isCustomFeeEdited =
feeSelectorUM.selectedFee?.amount?.value != feeSelectorUM.fees.normal.amount.value
if (feeSelectorUM.selectedType == FeeType.Custom && isCustomFeeEdited) {
analyticsEventHandler.send(
CommonSendFeeAnalyticEvents.GasPriceInserter(categoryName = analyticsCategoryName),
)
}
analyticsEventHandler.send(
CommonSendFeeAnalyticEvents.SelectedFee(
categoryName = analyticsCategoryName,
feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM),
source = params.analyticsSendSource,
),
)
if (feeSelectorUM.nonce != null) {
analyticsEventHandler.send(
CommonSendAnalyticEvents.NonceInserted(
categoryName = analyticsCategoryName,
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
)
}
saveResult()
}
}
}
private fun initialState() {
if (uiState.value is FeeUM.Empty) {
_uiState.update(
SendFeeInitialStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
appCurrency = appCurrency,
),
)
}
}
private fun subscribeOnFeeReloadTriggerUpdates() {
feeReloadListener.reloadTriggerFlow
.onEach { feeReload() }
.launchIn(modelScope)
}
private fun subscribeOnFeeCheckReloadTriggerUpdates() {
feeCheckReloadListener.checkReloadTriggerFlow
.onEach { checkLoadFee() }
.launchIn(modelScope)
}
private fun saveResult() {
_uiState.update(
SendFeeCustomAutoFixTransformer(
clickIntents = this@SendFeeModel,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
),
)
val params = params as? SendFeeComponentParams.FeeParams ?: return
params.callback.onFeeResult(uiState.value)
params.callback.onNextClick()
}
private fun checkLoadFee() {
modelScope.launch {
params.onLoadFee().fold(
ifRight = {
sendFeeAlertFactory.getFeeUpdatedAlert(
newFee = it,
feeUM = uiState.value,
proceedAction = {
modelScope.launch {
feeCheckReloadTrigger.callbackCheckResult(true)
}
},
stopAction = {
modelScope.launch {
feeCheckReloadTrigger.callbackCheckResult(false)
}
},
)
_uiState.update(
SendFeeLoadedTransformer(
fees = it,
clickIntents = this@SendFeeModel,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
isFeeApproximate = isFeeApproximate(it.normal.amount.type),
),
)
updateFeeNotifications()
},
ifLeft = { feeError ->
feeCheckReloadTrigger.callbackCheckResult(false)
_uiState.update(SendFeeFailedTransformer(feeError))
sendFeeAlertFactory.getFeeUnreachableErrorState(::feeReload)
updateFeeNotifications()
},
)
}.saveIn(feeJobHolder)
}
private fun isFeeApproximate(amountType: AmountType): Boolean {
val networkId = feeCryptoCurrencyStatus.currency.network.id
return isFeeApproximateUseCase(
networkId = networkId,
amountType = amountType,
)
}
private fun updateFeeNotifications() {
_uiState.update(
SendFeeNotificationsTransformer(
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
onFeeReload = ::feeReload,
),
)
}
private fun configFeeNavigation() {
val params = params as? SendFeeComponentParams.FeeParams ?: return
combine(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.common_fee_selector_title),
subtitle = null,
backIconRes = R.drawable.ic_back_24,
backIconClick = params.callback::onNextClick,
primaryButton = NavigationButton(
textReference = resourceReference(R.string.common_continue),
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onNextClick,
),
prevButton = null,
secondaryPairButtonsUM = null,
),
)
}.launchIn(modelScope)
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.converters
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
internal class FeeConverter(
clickIntents: SendFeeClickIntents,
appCurrency: AppCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val fees: TransactionFee,
) : Converter<FeeConverter.Data, Fee> {
private val customFeeConverter = SendFeeCustomFieldConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
normalFee = fees.normal,
)
override fun convert(value: Data): Fee {
return when (fees) {
is TransactionFee.Choosable -> {
when (value.feeType) {
FeeType.Slow -> fees.minimum
FeeType.Market -> fees.normal
FeeType.Fast -> fees.priority
FeeType.Custom -> customFeeConverter.convertBack(value.customValues)
}
}
is TransactionFee.Single ->
when (value.feeType) {
FeeType.Market -> fees.normal
FeeType.Custom -> customFeeConverter.convertBack(value.customValues)
else -> fees.normal
}
}
}
data class Data(
val customValues: ImmutableList<CustomFeeFieldUM>,
val feeType: FeeType,
)
}

View file

@ -1,114 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.converters
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.utils.converter.TwoWayConverter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal class SendFeeCustomFieldConverter(
private val clickIntents: SendFeeClickIntents,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val normalFee: Fee,
) : TwoWayConverter<Fee, ImmutableList<CustomFeeFieldUM>> {
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
EthereumCustomFeeConverter(
onCustomFeeValueChange = clickIntents::onCustomFeeValueChange,
onNextClick = clickIntents::onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
}
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
BitcoinCustomFeeConverter(
onCustomFeeValueChange = clickIntents::onCustomFeeValueChange,
onNextClick = clickIntents::onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
}
private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
KaspaCustomFeeConverter(
onCustomFeeValueChange = clickIntents::onCustomFeeValueChange,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
}
override fun convert(value: Fee): ImmutableList<CustomFeeFieldUM> {
return when (value) {
is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value)
is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value)
else -> persistentListOf()
}
}
override fun convertBack(value: ImmutableList<CustomFeeFieldUM>): Fee {
return if (value.isEmpty()) {
normalFee
} else {
when (normalFee) {
is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = value)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = value)
is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = value)
else -> {
val customFee = value.firstOrNull()
Fee.Common(
normalFee.amount.copy(
value = customFee?.value?.parseToBigDecimal(customFee.decimals),
),
)
}
}
}
}
fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) =
when (val fee = feeSelectorState.fees.normal) {
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
feeValue = fee,
customValues = feeSelectorState.customValues,
index = index,
value = value,
)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
customValues = feeSelectorState.customValues,
index = index,
value = value,
txSize = fee.txSize,
)
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
customValues = feeSelectorState.customValues,
index = index,
value = value,
)
else -> feeSelectorState.customValues
}
fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) {
is TransactionFee.Choosable -> feeSelectorState.fees.minimum
is TransactionFee.Single -> feeSelectorState.fees.normal
}.let {
when (it) {
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
minimumFee = it,
customValues = feeSelectorState.customValues,
)
else -> feeSelectorState.customValues
}
}
}

View file

@ -12,8 +12,8 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
import com.tangem.lib.crypto.BlockchainUtils
import kotlinx.collections.immutable.ImmutableList

View file

@ -11,8 +11,8 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

View file

@ -12,8 +12,8 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS

View file

@ -12,8 +12,8 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS

View file

@ -1,45 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
internal class SendFeeCustomAutoFixTransformer(
private val clickIntents: SendFeeClickIntents,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: return state
val customFeeConverter = SendFeeCustomFieldConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
normalFee = feeSelectorUM.fees.normal,
)
return when (feeSelectorUM.selectedType) {
FeeType.Slow,
FeeType.Market,
FeeType.Fast,
-> state
FeeType.Custom -> {
val updatedCustomValues = customFeeConverter.tryAutoFixValue(feeSelectorUM)
state.copy(
feeSelectorUM = feeSelectorUM.copy(
customValues = updatedCustomValues,
selectedFee = customFeeConverter.convertBack(updatedCustomValues),
),
)
}
}
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
internal class SendFeeCustomValueChangeTransformer(
private val index: Int,
private val value: String,
private val clickIntents: SendFeeClickIntents,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: return state
val customFeeConverter = SendFeeCustomFieldConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
normalFee = feeSelectorUM.fees.normal,
)
val updatedCustomValues = customFeeConverter.onValueChange(feeSelectorUM, index, value)
return state.copy(
feeSelectorUM = feeSelectorUM.copy(
customValues = updatedCustomValues,
selectedFee = customFeeConverter.convertBack(updatedCustomValues),
),
)
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
internal class SendFeeFailedTransformer(
private val error: GetFeeError,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
return state.copy(
feeSelectorUM = FeeSelectorUM.Error(error),
isPrimaryButtonEnabled = false,
)
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
internal class SendFeeInitialStateTransformer(
cryptoCurrencyStatus: CryptoCurrencyStatus,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
) : Transformer<FeeUM> {
private val cryptoCurrency = cryptoCurrencyStatus.currency
override fun transform(prevState: FeeUM): FeeUM {
return FeeUM.Content(
isPrimaryButtonEnabled = false,
feeSelectorUM = FeeSelectorUM.Loading,
notifications = persistentListOf(),
rate = feeCryptoCurrencyStatus.value.fiatRate,
appCurrency = appCurrency,
isFeeApproximate = false,
isCustomSelected = false,
isFeeConvertibleToFiat = feeCryptoCurrencyStatus.currency.network.hasFiatFeeRate,
isTronToken = cryptoCurrency is CryptoCurrency.Token &&
isTron(cryptoCurrency.network.rawId),
)
}
}

View file

@ -1,69 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
internal class SendFeeLoadedTransformer(
clickIntents: SendFeeClickIntents,
appCurrency: AppCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val fees: TransactionFee,
private val isFeeApproximate: Boolean,
) : Transformer<FeeUM> {
private val customFeeFieldConverter = SendFeeCustomFieldConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
normalFee = fees.normal,
)
private val feeConverter = FeeConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fees = fees,
)
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content
val updatedFeeSelector = if (feeSelectorUM == null) {
FeeSelectorUM.Content(
fees = fees,
customValues = customFeeFieldConverter.convert(fees.normal),
selectedFee = fees.normal,
nonce = prevState.nonce,
)
} else {
FeeSelectorUM.Content(
fees = fees,
customValues = feeSelectorUM.customValues,
selectedType = feeSelectorUM.selectedType,
selectedFee = feeConverter.convert(
FeeConverter.Data(
feeType = feeSelectorUM.selectedType,
customValues = feeSelectorUM.customValues,
),
),
nonce = prevState.nonce,
)
}
return state.copy(
feeSelectorUM = updatedFeeSelector,
isFeeApproximate = isFeeApproximate,
displayNonceInput = fees.normal is Fee.Ethereum,
nonce = prevState.nonce,
)
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
internal object SendFeeLoadingTransformer : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
return state.copy(
feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: FeeSelectorUM.Loading,
notifications = persistentListOf(),
isPrimaryButtonEnabled = false,
)
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import java.math.BigInteger
internal class SendFeeNonceChangeTransformer(
private val value: String,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
if (value.isEmpty()) {
return state.copy(nonce = null)
}
return try {
val nonce = BigInteger(value)
state.copy(
nonce = nonce,
feeSelectorUM = when (val feeSelectorUM = state.feeSelectorUM) {
is FeeSelectorUM.Loading,
is FeeSelectorUM.Error,
-> feeSelectorUM
is FeeSelectorUM.Content -> feeSelectorUM.copy(
nonce = nonce,
)
},
)
} catch (e: NumberFormatException) {
state
}
}
}

View file

@ -1,51 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.extensions.isZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
internal class SendFeeNotificationsTransformer(
private val cryptoCurrencyName: String,
private val onFeeReload: () -> Unit,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
val notifications = state.getNotifications()
return state.copy(
notifications = notifications,
isPrimaryButtonEnabled = state.isPrimaryButtonEnabled(notifications),
)
}
private fun FeeUM.Content.getNotifications() = buildList {
addFeeUnreachableNotification(
feeError = (feeSelectorUM as? FeeSelectorUM.Error)?.error,
tokenName = cryptoCurrencyName,
onReload = onFeeReload,
)
}.toImmutableList()
private fun FeeUM.Content.isPrimaryButtonEnabled(notifications: ImmutableList<NotificationUM>): Boolean {
val feeSelectorState = feeSelectorUM as? FeeSelectorUM.Content ?: return false
val customValue = feeSelectorState.customValues.firstOrNull()
val isNotCustom = feeSelectorState.selectedType != FeeType.Custom
val isNotEmptyCustom = if (customValue != null) {
!customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom
} else {
false
}
val noErrors = notifications.none { it is NotificationUM.Error }
return noErrors && (isNotEmptyCustom || isNotCustom)
}
}

View file

@ -1,46 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
internal class SendFeeSelectTransformer(
private val feeType: FeeType,
private val clickIntents: SendFeeClickIntents,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: return state
val feeConverter = FeeConverter(
clickIntents = clickIntents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fees = feeSelectorUM.fees,
)
val updatedFeeSelectorState = feeSelectorUM.copy(
selectedType = feeType,
selectedFee = feeConverter.convert(
FeeConverter.Data(
customValues = feeSelectorUM.customValues,
feeType = feeType,
),
),
)
val isCustomFeeWasSelected = state.isCustomSelected || updatedFeeSelectorState.selectedType == FeeType.Custom
return state.copy(
isCustomSelected = isCustomFeeWasSelected,
feeSelectorUM = updatedFeeSelectorState,
)
}
}

View file

@ -1,132 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@Composable
internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit) {
val feeUM = feeUM as? FeeUM.Content ?: return
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content
val isEditingDisabled = feeSelectorUM?.fees is TransactionFee.Single
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = isClickEnabled && !isEditingDisabled, onClick = onClick)
.padding(12.dp),
) {
Text(
text = stringResourceSafe(R.string.common_network_fee_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.secondary,
)
Box(
modifier = Modifier.padding(top = 8.dp),
) {
val feeSelectorUM = feeUM.feeSelectorUM
val feeAmount = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee?.amount
val (title, icon) = if (feeSelectorUM is FeeSelectorUM.Content) {
when (feeSelectorUM.selectedType) {
FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24
FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24
FeeType.Custom -> R.string.common_custom to R.drawable.ic_edit_24
}
} else {
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
}
SelectorRowItem(
title = resourceReference(title),
iconRes = icon,
preDot = remember {
stringReference(
feeAmount?.value.format {
crypto(
symbol = feeAmount?.currencySymbol.orEmpty(),
decimals = feeAmount?.decimals ?: 0,
).fee(canBeLower = feeUM.isFeeApproximate)
},
)
},
postDot = remember {
if (feeUM.isFeeConvertibleToFiat) {
getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency)
} else {
null
}
},
ellipsizeOffset = feeAmount?.currencySymbol?.length,
isSelected = true,
showDivider = false,
showSelectedAppearance = false,
paddingValues = PaddingValues(),
)
FeeLoading(feeSelectorUM)
FeeError(feeSelectorUM)
}
}
}
@Composable
private fun BoxScope.FeeLoading(feeSelectorUM: FeeSelectorUM) {
AnimatedContent(
targetState = feeSelectorUM,
label = "Fee Loading State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == FeeSelectorUM.Loading) {
RectangleShimmer(
radius = 3.dp,
modifier = Modifier.size(
height = 12.dp,
width = 90.dp,
),
)
}
}
}
@Composable
private fun BoxScope.FeeError(feeSelectorUM: FeeSelectorUM) {
AnimatedContent(
targetState = feeSelectorUM,
label = "Fee Error State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it is FeeSelectorUM.Error) {
Text(
text = EMPTY_BALANCE_SIGN,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowEnter
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import kotlinx.collections.immutable.ImmutableList
@Suppress("LongParameterList")
@Composable
internal fun SendCustomFee(
customValues: ImmutableList<CustomFeeFieldUM>,
selectedFee: FeeType,
hasNotifications: Boolean,
onValueChange: (Int, String) -> Unit,
onNonceChange: (String) -> Unit,
displayNonceInput: Boolean,
nonce: String?,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = selectedFee == FeeType.Custom && customValues.isNotEmpty(),
label = "Custom Fee Selected Animation",
enter = expandVertically().plus(fadeIn()),
exit = shrinkVertically().plus(fadeOut()),
) {
val bottomPadding = if (hasNotifications) 12.dp else 0.dp
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier.padding(bottom = bottomPadding),
) {
repeat(customValues.size) { index ->
val value = customValues[index]
FooterContainer(
footer = value.footer,
) {
if (value.label != null) {
InputRowEnterInfoAmount(
text = value.value,
decimals = value.decimals,
symbol = value.symbol,
title = value.title,
info = value.label,
keyboardOptions = value.keyboardOptions,
keyboardActions = value.keyboardActions,
onValueChange = { onValueChange(index, it) },
showDivider = false,
isReadOnly = value.isReadonly,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
} else {
InputRowEnterAmount(
text = value.value,
decimals = value.decimals,
title = value.title,
symbol = value.symbol,
onValueChange = { onValueChange(index, it) },
keyboardOptions = value.keyboardOptions,
keyboardActions = value.keyboardActions,
showDivider = false,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}
}
if (displayNonceInput) {
Nonce(
onNonceChange = onNonceChange,
nonce = nonce,
)
}
}
}
}
@Composable
private fun Nonce(onNonceChange: (String) -> Unit, nonce: String?) {
FooterContainer(
footer = resourceReference(R.string.send_nonce_footer),
modifier = Modifier.padding(bottom = 12.dp),
) {
InputRowEnter(
text = nonce.orEmpty(),
title = resourceReference(R.string.send_nonce),
onValueChange = onNonceChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
placeholder = resourceReference(R.string.send_nonce_hint),
showDivider = false,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}

View file

@ -1,89 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.notifications
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
@Composable
internal fun SendFeeContent(state: FeeUM, clickIntents: SendFeeClickIntents) {
if (state !is FeeUM.Content) return
val notifications = state.notifications
val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content
val isCustomSelected = feeSelectorUM?.selectedType == FeeType.Custom
val hasNotifications = notifications.isNotEmpty()
LazyColumn(
modifier = Modifier // Do not put fillMaxSize() in here
.background(TangemTheme.colors.background.tertiary)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
),
) {
feeSelector(state, clickIntents)
if (feeSelectorUM != null) {
customFee(
feeSelectorUM = feeSelectorUM,
onValueChange = clickIntents::onCustomFeeValueChange,
onNonceChange = clickIntents::onNonceChange,
displayNonceInput = state.displayNonceInput,
nonce = state.nonce?.toString().orEmpty(),
hasNotifications = hasNotifications,
)
}
notifications(notifications = notifications, hasPaddingAbove = isCustomSelected)
}
}
private fun LazyListScope.feeSelector(state: FeeUM.Content, clickIntents: SendFeeClickIntents) {
item(key = FEE_SELECTOR_KEY) {
SendSpeedSelector(
state = state,
clickIntents = clickIntents,
modifier = Modifier.animateItem(),
)
}
}
@Suppress("LongParameterList")
internal fun LazyListScope.customFee(
feeSelectorUM: FeeSelectorUM.Content,
hasNotifications: Boolean,
onValueChange: (Int, String) -> Unit,
onNonceChange: (String) -> Unit,
displayNonceInput: Boolean,
nonce: String?,
modifier: Modifier = Modifier,
) {
item(key = FEE_CUSTOM_KEY) {
SendCustomFee(
customValues = feeSelectorUM.customValues,
selectedFee = feeSelectorUM.selectedType,
hasNotifications = hasNotifications,
onValueChange = onValueChange,
onNonceChange = onNonceChange,
displayNonceInput = displayNonceInput,
nonce = nonce,
modifier = modifier
.fillMaxWidth()
.animateItem()
.background(TangemTheme.colors.background.tertiary)
.padding(top = 12.dp),
)
}
}

View file

@ -1,102 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
@Suppress("LongMethod")
@Composable
internal fun SendSpeedSelector(
state: FeeUM.Content,
clickIntents: SendFeeClickIntents,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_slow,
iconRes = R.drawable.ic_tortoise_24,
feeType = FeeType.Slow,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_market,
iconRes = R.drawable.ic_bird_24,
feeType = FeeType.Market,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_fast,
iconRes = R.drawable.ic_hare_24,
feeType = FeeType.Fast,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_custom,
iconRes = R.drawable.ic_edit_24,
feeType = FeeType.Custom,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) },
)
}
FooterText(clickIntents::onReadMoreClick)
}
}
@Composable
private fun FooterText(onReadMoreClick: () -> Unit) {
val linkText = stringResourceSafe(R.string.common_read_more)
val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText)
val linkTextPosition = fullString.length - linkText.length
val defaultStyle = TangemTheme.colors.text.tertiary
val linkStyle = TangemTheme.colors.text.accent
val annotatedString = remember(defaultStyle, linkStyle) {
buildAnnotatedString {
withStyle(SpanStyle(defaultStyle)) {
append(fullString.substring(0, linkTextPosition))
}
withStyle(SpanStyle(linkStyle)) {
append(fullString.substring(linkTextPosition, fullString.length))
}
}
}
val click = { i: Int ->
val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1))
if (i in readMoreStyle.start..readMoreStyle.end) {
onReadMoreClick()
}
}
ClickableText(
text = annotatedString,
style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start),
modifier = Modifier.padding(top = 8.dp),
onClick = click,
)
}

View file

@ -1,153 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@Composable
internal fun SendSpeedSelectorItem(
@StringRes titleRes: Int,
@DrawableRes iconRes: Int,
feeType: FeeType,
state: FeeUM.Content,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
) {
val feeSelectorState = state.feeSelectorUM
val content = feeSelectorState as? FeeSelectorUM.Content
val amount = content?.getAmount(feeType)
val (showDivider, isVisible) = content.getDividerAndVisibility(feeType)
AnimatedVisibility(
visible = isVisible,
label = "Fee Selector Visibility Animation",
enter = expandVertically().plus(fadeIn()),
exit = shrinkVertically().plus(fadeOut()),
) {
Box(
modifier = modifier
.fillMaxWidth()
.clickable { onSelect() },
) {
SelectorRowItem(
title = resourceReference(titleRes),
iconRes = iconRes,
onSelect = onSelect,
modifier = modifier,
preDot = stringReference(
amount?.value.format {
crypto(
symbol = amount?.currencySymbol.orEmpty(),
decimals = amount?.decimals ?: 0,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.isFeeConvertibleToFiat) {
getFiatReference(amount?.value, state.rate, state.appCurrency)
} else {
null
},
ellipsizeOffset = amount?.currencySymbol?.length,
isSelected = content?.selectedType == feeType,
showDivider = showDivider,
)
FeeLoading(feeSelectorState)
FeeError(feeSelectorState)
}
}
}
@Composable
private fun FeeLoading(feeSelectorState: FeeSelectorUM) {
Row {
SpacerWMax()
AnimatedVisibility(
visible = feeSelectorState == FeeSelectorUM.Loading,
label = "Fee Loading State Change",
modifier = Modifier.align(Alignment.CenterVertically),
) {
RectangleShimmer(
radius = 3.dp,
modifier = Modifier
.padding(
vertical = 18.dp,
horizontal = 12.dp,
)
.size(
height = 12.dp,
width = 90.dp,
),
)
}
}
}
@Composable
private fun FeeError(feeSelectorState: FeeSelectorUM) {
Row {
SpacerWMax()
AnimatedVisibility(
visible = feeSelectorState is FeeSelectorUM.Error,
label = "Fee Error State Change",
modifier = Modifier.align(Alignment.CenterVertically),
) {
Text(
text = EMPTY_BALANCE_SIGN,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
modifier = Modifier
.padding(
vertical = 14.dp,
horizontal = 12.dp,
),
)
}
}
}
private fun FeeSelectorUM.Content.getAmount(feeType: FeeType): Amount? {
val choosableFees = fees as? TransactionFee.Choosable
val decimals = fees.normal.amount.decimals
val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals)
val customAmount = fees.normal.amount.copy(value = customValue)
return when (feeType) {
FeeType.Slow -> choosableFees?.minimum?.amount
FeeType.Market -> fees.normal.amount
FeeType.Fast -> choosableFees?.priority?.amount
FeeType.Custom -> customAmount
}
}
private fun FeeSelectorUM.Content?.getDividerAndVisibility(feeType: FeeType): Pair<Boolean, Boolean> {
val hasCustomValues = !this?.customValues.isNullOrEmpty()
val isNotSingle = this?.fees !is TransactionFee.Single
return when (feeType) {
FeeType.Slow -> true to isNotSingle
FeeType.Market -> (isNotSingle || hasCustomValues) to true
FeeType.Fast -> hasCustomValues to isNotSingle
FeeType.Custom -> false to hasCustomValues
}
}

View file

@ -1,47 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui.state
import androidx.compose.runtime.Stable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigInteger
@Stable
internal sealed class FeeSelectorUM {
data class Content(
val fees: TransactionFee,
val selectedType: FeeType = FeeType.Market,
val selectedFee: Fee?,
val customValues: ImmutableList<CustomFeeFieldUM> = persistentListOf(),
val nonce: BigInteger? = null,
) : FeeSelectorUM()
data object Loading : FeeSelectorUM()
data class Error(
val error: GetFeeError,
) : FeeSelectorUM()
}
internal enum class FeeType {
Slow,
Market,
Fast,
Custom,
;
fun toAnalyticType(feeSelectorUM: FeeSelectorUM.Content): AnalyticsParam.FeeType = when (feeSelectorUM.fees) {
is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed
is TransactionFee.Choosable -> when (feeSelectorUM.selectedType) {
Slow -> AnalyticsParam.FeeType.Min
Market -> AnalyticsParam.FeeType.Normal
Fast -> AnalyticsParam.FeeType.Max
Custom -> AnalyticsParam.FeeType.Custom
}
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.features.send.v2.subcomponents.fee.ui.state
import androidx.compose.runtime.Stable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.BigInteger
@Stable
internal sealed class FeeUM {
abstract val isPrimaryButtonEnabled: Boolean
data class Content(
override val isPrimaryButtonEnabled: Boolean,
val feeSelectorUM: FeeSelectorUM,
val rate: BigDecimal?,
val appCurrency: AppCurrency,
val isFeeConvertibleToFiat: Boolean,
val isFeeApproximate: Boolean,
val isCustomSelected: Boolean,
val isTronToken: Boolean,
val customValues: ImmutableList<CustomFeeFieldUM> = persistentListOf(),
val displayNonceInput: Boolean = false,
val nonce: BigInteger? = null,
val notifications: ImmutableList<NotificationUM>,
val isEditingDisabled: Boolean = false,
) : FeeUM()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : FeeUM()
}

View file

@ -36,10 +36,10 @@ import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkFeeCoverage
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage
import com.tangem.features.send.v2.subcomponents.notifications.analytics.NotificationsAnalyticEvents
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.BlockchainUtils.isTron

View file

@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.api.entity.*
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType

View file

@ -9,7 +9,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM

View file

@ -50,7 +50,6 @@ internal class SwapAmountFieldConverter(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus),
iconStateConverter = iconStateConverter,
isRedesignEnabled = true,
isBalanceHidden = isBalanceHidden,
).convert(
AmountParameters(

View file

@ -311,7 +311,6 @@ private fun SendWithSwapSuccessContent_Preview() {
networkName = "Polygon",
isValidating = false,
isInitialized = false,
isRedesignEnabled = false,
isRecentHidden = false,
),
feeSelectorUM = FeeSelectorUM.Content(

View file

@ -8,7 +8,6 @@ import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.*
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
@ -81,7 +80,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala
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.features.send.v2.api.SendFeatureToggles
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
@ -140,7 +138,6 @@ internal class TokenDetailsModel @Inject constructor(
private val router: InnerTokenDetailsRouter,
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val sendFeatureToggles: SendFeatureToggles,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
@ -551,17 +548,10 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun sendCurrency() {
val route = if (sendFeatureToggles.isSendWithSwapEnabled) {
AppRoute.SendEntryPoint(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
} else {
AppRoute.Send(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
}
val route = AppRoute.SendEntryPoint(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
appRouter.push(route)
}
@ -1181,7 +1171,7 @@ internal class TokenDetailsModel @Inject constructor(
TokenAction.Receive -> navigateToReceive()
TokenAction.Send -> sendCurrency()
TokenAction.Swap -> appRouter.push(
Swap(
AppRoute.Swap(
currencyFrom = cryptoCurrency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.Token.value,