Updated on 2026-08-14
This commit is contained in:
commit
80d4eeaed9
41 changed files with 2811 additions and 5 deletions
|
|
@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
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 dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -29,4 +30,9 @@ internal interface SendModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(SendDestinationModel::class)
|
||||
fun provideSendDestinationModel(model: SendDestinationModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendFeeModel::class)
|
||||
fun provideSendFeeModel(model: SendFeeModel): Model
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
|
|||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
|
|
@ -17,6 +18,9 @@ import com.tangem.features.send.v2.send.model.SendModel
|
|||
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.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
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
|
||||
|
|
@ -66,7 +70,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
SendRoute.Empty -> getStubComponent()
|
||||
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
|
||||
is SendRoute.Amount -> getAmountComponent(factoryContext, route)
|
||||
is SendRoute.Fee -> getFeeComponent()
|
||||
is SendRoute.Fee -> getFeeComponent(factoryContext)
|
||||
SendRoute.Confirm -> getConfirmComponent()
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +103,30 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private fun getFeeComponent() = getStubComponent() // todo
|
||||
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?.value
|
||||
return if (sendAmount != null && destinationAddress != null) {
|
||||
SendFeeComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendFeeComponentParams.FeeParams(
|
||||
state = model.uiState.value.feeUM,
|
||||
currentRoute = currentRoute.filterIsInstance<SendRoute.Fee>(),
|
||||
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
|
||||
appCurrency = model.appCurrency,
|
||||
sendAmount = sendAmount,
|
||||
destinationAddress = destinationAddress,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
getStubComponent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfirmComponent() = getStubComponent() // todo
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ 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.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -25,13 +27,15 @@ internal class SendModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model(),
|
||||
SendDestinationComponent.ModelCallback,
|
||||
SendAmountComponent.ModelCallback {
|
||||
SendAmountComponent.ModelCallback,
|
||||
SendFeeComponent.ModelCallback {
|
||||
|
||||
private val _uiState = MutableStateFlow(initialState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
var userWallet: UserWallet by Delegates.notNull()
|
||||
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
var appCurrency: AppCurrency = AppCurrency.Default
|
||||
var predefinedAmountValue: String? = null
|
||||
|
||||
|
|
@ -39,12 +43,17 @@ internal class SendModel @Inject constructor(
|
|||
_uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onAmountResult(state: AmountState) {
|
||||
_uiState.update { it.copy(amountUM = state) }
|
||||
override fun onAmountResult(amountUM: AmountState) {
|
||||
_uiState.update { it.copy(amountUM = amountUM) }
|
||||
}
|
||||
|
||||
override fun onFeeResult(feeUM: FeeUM) {
|
||||
_uiState.update { it.copy(feeUM = feeUM) }
|
||||
}
|
||||
|
||||
private fun initialState(): SendUM = SendUM(
|
||||
amountUM = AmountState.Empty(),
|
||||
destinationUM = DestinationUM.Empty(),
|
||||
feeUM = FeeUM.Empty(),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@ package com.tangem.features.send.v2.send.ui.state
|
|||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
|
||||
internal data class SendUM(
|
||||
val amountUM: AmountState,
|
||||
val destinationUM: DestinationUM,
|
||||
val feeUM: FeeUM,
|
||||
)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.features.send.v2.subcomponents
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal fun LazyListScope.notifications(
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
hasPaddingAbove: Boolean = false,
|
||||
isClickDisabled: Boolean = false,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = notifications,
|
||||
key = { _, item -> item::class.java },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { i, item ->
|
||||
val topPadding = if (i == 0 && hasPaddingAbove) 0.dp else 12.dp
|
||||
Notification(
|
||||
config = item.config,
|
||||
modifier = modifier
|
||||
.padding(top = topPadding)
|
||||
.animateItem(),
|
||||
containerColor = when (item) {
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning.NetworkFeeUnreachable,
|
||||
is NotificationUM.Warning.HighFeeError,
|
||||
-> TangemTheme.colors.background.action
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (item) {
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning,
|
||||
-> null
|
||||
is NotificationUM.Error -> TangemTheme.colors.icon.warning
|
||||
is NotificationUM.Info -> TangemTheme.colors.icon.accent
|
||||
},
|
||||
isEnabled = !isClickDisabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@ internal class SendDestinationComponent(
|
|||
|
||||
private val model: SendDestinationModel = getOrCreateModel(params = params, router = router)
|
||||
|
||||
fun updateState(state: DestinationUM) = model.updateState(state)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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.state.FeeUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.SendFeeContent
|
||||
|
||||
internal class SendFeeComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val 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 {
|
||||
fun onFeeResult(state: FeeUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
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
|
||||
|
||||
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,
|
||||
val currentRoute: Flow<SendRoute.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,
|
||||
val blockClickEnableFlow: StateFlow<Boolean>,
|
||||
) : SendFeeComponentParams()
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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 {
|
||||
|
||||
/** Flow triggers fee reload */
|
||||
val reloadTriggerFlow: Flow<Unit>
|
||||
|
||||
/** Trigger fee update */
|
||||
suspend fun triggerUpdate()
|
||||
}
|
||||
|
||||
interface SendFeeCheckReloadTrigger {
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
||||
/** Trigger return callback with check result */
|
||||
suspend fun callbackCheckResult(isSuccess: Boolean)
|
||||
|
||||
/** Trigger fee check reload */
|
||||
suspend fun triggerCheckUpdate()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class DefaultSendFeeReloadTrigger @Inject constructor() : SendFeeReloadTrigger, SendFeeCheckReloadTrigger {
|
||||
|
||||
private val _reloadTriggerFlow = MutableSharedFlow<Unit>()
|
||||
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() {
|
||||
_reloadTriggerFlow.emit(Unit)
|
||||
}
|
||||
|
||||
override suspend fun triggerCheckUpdate() {
|
||||
_checkReloadTriggerFlow.emit(Unit)
|
||||
}
|
||||
|
||||
override suspend fun callbackCheckResult(isSuccess: Boolean) {
|
||||
_checkReloadResultFlow.emit(isSuccess)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
internal sealed class SendFeeAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = category, event = event, params = params) {
|
||||
|
||||
abstract val categoryName: String
|
||||
|
||||
/** Selected fee (send after next screen opened) */
|
||||
data class SelectedFee(
|
||||
override val categoryName: String,
|
||||
val feeType: AnalyticsParam.FeeType,
|
||||
) : SendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Selected",
|
||||
params = mapOf("Fee Type" to feeType.value),
|
||||
)
|
||||
|
||||
/** Custom fee selected */
|
||||
data class CustomFeeButtonClicked(
|
||||
override val categoryName: String,
|
||||
) : SendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Custom Fee Clicked",
|
||||
)
|
||||
|
||||
/** Custom fee edited */
|
||||
data class GasPriceInserter(
|
||||
override val categoryName: String,
|
||||
) : SendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Gas Price Inserted",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.di
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.fee.DefaultSendFeeReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal object SendFeeModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendFeeReloadTrigger(): SendFeeReloadTrigger {
|
||||
return DefaultSendFeeReloadTrigger()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendFeeCheckReloadTrigger(): SendFeeCheckReloadTrigger {
|
||||
return DefaultSendFeeReloadTrigger()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
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.tokens.model.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
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
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, onFeeNotIncreased: () -> 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,
|
||||
firstActionBuilder = { okAction() },
|
||||
secondActionBuilder = { cancelAction() },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onFeeNotIncreased()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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 onReadMoreClick()
|
||||
|
||||
fun onNextClick()
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
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.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
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.SendFeeReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents.GasPriceInserter
|
||||
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.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 java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class SendFeeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeeAlertFactory: SendFeeAlertFactory,
|
||||
private val feeReloadTrigger: SendFeeReloadTrigger,
|
||||
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()
|
||||
loadFee()
|
||||
}
|
||||
|
||||
fun updateState(state: FeeUM) {
|
||||
_uiState.value = state
|
||||
}
|
||||
|
||||
override fun feeReload() = loadFee()
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
_uiState.update(
|
||||
SendFeeSelectTransformer(
|
||||
feeType = feeType,
|
||||
clickIntents = this@SendFeeModel,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
updateFeeNotifications()
|
||||
if (feeType == FeeType.Custom) {
|
||||
analyticsEventHandler.send(
|
||||
SendFeeAnalyticEvents.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 onReadMoreClick() {
|
||||
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
|
||||
val url = buildString {
|
||||
append(FEE_READ_MORE_URL_FIRST_PART)
|
||||
append(locale)
|
||||
append(FEE_READ_MORE_URL_SECOND_PART)
|
||||
}
|
||||
urlOpener.openUrl(url)
|
||||
}
|
||||
|
||||
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(GasPriceInserter(categoryName = analyticsCategoryName))
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
SendFeeAnalyticEvents.SelectedFee(
|
||||
categoryName = analyticsCategoryName,
|
||||
feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM),
|
||||
),
|
||||
)
|
||||
|
||||
navigate()
|
||||
}
|
||||
} else {
|
||||
navigate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
if (uiState.value is FeeUM.Empty) {
|
||||
_uiState.update(
|
||||
SendFeeInitialStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeReloadTriggerUpdates() {
|
||||
feeReloadTrigger.reloadTriggerFlow
|
||||
.onEach { feeReload() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeCheckReloadTriggerUpdates() {
|
||||
feeCheckReloadTrigger.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)
|
||||
}
|
||||
|
||||
private fun loadFee() {
|
||||
modelScope.launch {
|
||||
val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content
|
||||
if (isShowLoading) {
|
||||
_uiState.update(SendFeeLoadingTransformer)
|
||||
}
|
||||
callFeeUseCase().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)
|
||||
}
|
||||
|
||||
private fun checkLoadFee() {
|
||||
modelScope.launch {
|
||||
callFeeUseCase().fold(
|
||||
ifRight = {
|
||||
feeCheckReloadTrigger.callbackCheckResult(true)
|
||||
_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(::checkLoadFee)
|
||||
updateFeeNotifications()
|
||||
},
|
||||
)
|
||||
}.saveIn(feeJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun callFeeUseCase() = getFeeUseCase.invoke(
|
||||
amount = params.sendAmount,
|
||||
destination = params.destinationAddress,
|
||||
userWallet = params.userWallet,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
)
|
||||
|
||||
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 navigate() {
|
||||
saveResult()
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun configFeeNavigation() {
|
||||
val params = params as? SendFeeComponentParams.FeeParams ?: return
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (_, _) ->
|
||||
// todo
|
||||
// params.callback.onNavigationResult(
|
||||
// NavigationUM.Content(
|
||||
// title = resourceReference(R.string.common_fee_selector_title),
|
||||
// subtitle = null,
|
||||
// backIconRes = R.drawable.ic_back_24,
|
||||
// backIconClick = router::pop,
|
||||
// primaryButton = ButtonsUM.PrimaryButtonUM(
|
||||
// text = resourceReference(R.string.common_continue),
|
||||
// isEnabled = state.isPrimaryButtonEnabled,
|
||||
// onClick = ::onNextClick,
|
||||
// ),
|
||||
// prevButton = null,
|
||||
// secondaryPairButtonsUM = null,
|
||||
// ),
|
||||
// )
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val RU_LOCALE = "ru"
|
||||
const val EN_LOCALE = "en"
|
||||
const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/"
|
||||
const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
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.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
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.tokens.model.CryptoCurrencyStatus
|
||||
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.CustomFeeFieldUM
|
||||
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(
|
||||
clickIntents = clickIntents,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
|
||||
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BitcoinCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
|
||||
private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
KaspaCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal interface CustomFeeConverter<T : Fee> : Converter<T, ImmutableList<CustomFeeFieldUM>> {
|
||||
fun convertBack(normalFee: T, value: ImmutableList<CustomFeeFieldUM>): T
|
||||
}
|
||||
|
||||
internal fun MutableList<CustomFeeFieldUM>.setEmpty(index: Int) {
|
||||
set(index, this[index].copy(value = ""))
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
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.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class BitcoinCustomFeeConverter(
|
||||
private val clickIntents: SendFeeClickIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : CustomFeeConverter<Fee.Bitcoin> {
|
||||
|
||||
private val currencyStatus = feeCryptoCurrencyStatus.value
|
||||
private val network = feeCryptoCurrencyStatus.currency.network.id.value
|
||||
|
||||
override fun convert(value: Fee.Bitcoin): ImmutableList<CustomFeeFieldUM> {
|
||||
val feeValue = value.amount.value
|
||||
return if (BlockchainUtils.isUseBitcoinFeeConverter(network)) {
|
||||
persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Companion.Next,
|
||||
keyboardType = KeyboardType.Companion.Number,
|
||||
),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_bitcoin_custom_fee_footer),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = feeValue,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
isReadonly = true,
|
||||
),
|
||||
CustomFeeFieldUM(
|
||||
value = toSatoshiPerByte(
|
||||
amount = feeValue,
|
||||
decimals = value.amount.decimals,
|
||||
txSize = value.txSize,
|
||||
).toString(),
|
||||
decimals = SATOSHI_DECIMALS,
|
||||
symbol = "",
|
||||
title = resourceReference(R.string.send_satoshi_per_byte_title),
|
||||
footer = resourceReference(R.string.send_satoshi_per_byte_text),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (checkExceedBalance(
|
||||
feeBalance = currencyStatus.amount,
|
||||
feeAmount = feeValue,
|
||||
)
|
||||
) {
|
||||
ImeAction.Companion.None
|
||||
} else {
|
||||
ImeAction.Companion.Done
|
||||
},
|
||||
keyboardType = KeyboardType.Companion.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { clickIntents.onNextClick() },
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Bitcoin, value: ImmutableList<CustomFeeFieldUM>): Fee.Bitcoin {
|
||||
val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(value[FEE_AMOUNT_INDEX].decimals)
|
||||
val satoshiPerByte = value[FEE_SATOSHI_INDEX].value.parseToBigDecimal(value[FEE_SATOSHI_INDEX].decimals)
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
satoshiPerByte = satoshiPerByte,
|
||||
)
|
||||
}
|
||||
|
||||
fun onValueChange(
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
txSize: BigDecimal,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
if (index == FEE_SATOSHI_INDEX) {
|
||||
val newSatoshiPerKb = value.parseToBigDecimal(this[FEE_SATOSHI_INDEX].decimals)
|
||||
val newFeeAmount = newSatoshiPerKb.multiply(txSize)
|
||||
.movePointLeft(this[FEE_AMOUNT_INDEX].decimals)
|
||||
.setScale(this[FEE_AMOUNT_INDEX].decimals, RoundingMode.DOWN)
|
||||
set(
|
||||
FEE_AMOUNT_INDEX,
|
||||
this[FEE_AMOUNT_INDEX].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals),
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatus.value.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? {
|
||||
val newFeeAmount = amount?.movePointRight(decimals)
|
||||
return newFeeAmount?.divide(
|
||||
txSize,
|
||||
SATOSHI_DECIMALS,
|
||||
RoundingMode.HALF_UP,
|
||||
)?.setScale(SATOSHI_DECIMALS, RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val FEE_AMOUNT_INDEX = 0
|
||||
private const val FEE_SATOSHI_INDEX = 1
|
||||
private const val SATOSHI_DECIMALS = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Base ethereum custom fee converter
|
||||
*
|
||||
* @param T subtype of [Fee.Ethereum]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface BaseEthereumCustomFeeConverter<T : Fee.Ethereum> : CustomFeeConverter<T> {
|
||||
|
||||
fun getGasLimitIndex(feeValue: T): Int
|
||||
|
||||
fun onValueChange(
|
||||
feeValue: T,
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<CustomFeeFieldUM>
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class EthereumCustomFeeConverter(
|
||||
private val clickIntents: SendFeeClickIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : BaseEthereumCustomFeeConverter<Fee.Ethereum> {
|
||||
|
||||
private val currencyStatus = feeCryptoCurrencyStatus.value
|
||||
|
||||
private val legacyFeeConverter = EthereumLegacyCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
private val eipFeeConverter = EthereumEIPCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
override fun convert(value: Fee.Ethereum): ImmutableList<CustomFeeFieldUM> {
|
||||
return buildList {
|
||||
convertFeeValue(value).let(::add)
|
||||
|
||||
when (value) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value)
|
||||
}.let(::addAll)
|
||||
|
||||
convertGasLimitValue(value).let(::add)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList<CustomFeeFieldUM>): Fee.Ethereum {
|
||||
return when (normalFee) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.convertBack(normalFee = normalFee, value = value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.convertBack(normalFee = normalFee, value = value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum): Int {
|
||||
return when (feeValue) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.getGasLimitIndex(feeValue)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.getGasLimitIndex(feeValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValueChange(
|
||||
feeValue: Fee.Ethereum,
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
return when (feeValue) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange(feeValue, customValues, index, value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange(feeValue, customValues, index, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertFeeValue(value: Fee.Ethereum): CustomFeeFieldUM {
|
||||
val feeValue = value.amount.value
|
||||
|
||||
return CustomFeeFieldUM(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_amount_fee_footer),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = feeValue,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertGasLimitValue(value: Fee.Ethereum): CustomFeeFieldUM {
|
||||
val isExceedBalance = checkExceedBalance(feeBalance = currencyStatus.amount, feeAmount = value.amount.value)
|
||||
|
||||
return CustomFeeFieldUM(
|
||||
value = value.gasLimit.toString(),
|
||||
decimals = GAS_DECIMALS,
|
||||
symbol = "",
|
||||
title = resourceReference(R.string.send_gas_limit),
|
||||
footer = resourceReference(R.string.send_gas_limit_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(getGasLimitIndex(value), it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (isExceedBalance) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { clickIntents.onNextClick() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ETHEREUM_GAS_UNIT = "GWEI"
|
||||
const val GIGA_DECIMALS = 9
|
||||
const val GAS_DECIMALS = 0
|
||||
const val FEE_AMOUNT = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
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
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class EthereumEIPCustomFeeConverter(
|
||||
private val clickIntents: SendFeeClickIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : BaseEthereumCustomFeeConverter<Fee.Ethereum.EIP1559> {
|
||||
|
||||
private val currencyStatus = feeCryptoCurrencyStatus.value
|
||||
|
||||
override fun convert(value: Fee.Ethereum.EIP1559): ImmutableList<CustomFeeFieldUM> {
|
||||
return persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = value.maxFeePerGas.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_custom_evm_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_evm_max_fee_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(MAX_FEE, it) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
CustomFeeFieldUM(
|
||||
value = value.priorityFee.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_custom_evm_priority_fee),
|
||||
footer = resourceReference(R.string.send_custom_evm_priority_fee_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(PRIORITY_FEE, it) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(
|
||||
normalFee: Fee.Ethereum.EIP1559,
|
||||
value: ImmutableList<CustomFeeFieldUM>,
|
||||
): Fee.Ethereum.EIP1559 {
|
||||
val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals)
|
||||
val maxFeeDecimals = value[MAX_FEE].decimals
|
||||
val maxFee = value[MAX_FEE].value.parseToBigDecimal(maxFeeDecimals)
|
||||
.movePointRight(maxFeeDecimals)
|
||||
.toBigInteger()
|
||||
val priorityFeeDecimals = value[PRIORITY_FEE].decimals
|
||||
val priorityFee = value[PRIORITY_FEE].value.parseToBigDecimal(priorityFeeDecimals)
|
||||
.movePointRight(priorityFeeDecimals)
|
||||
.toBigInteger()
|
||||
val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
maxFeePerGas = maxFee,
|
||||
priorityFee = priorityFee,
|
||||
gasLimit = gasLimit,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum.EIP1559): Int = GAS_LIMIT
|
||||
|
||||
override fun onValueChange(
|
||||
feeValue: Fee.Ethereum.EIP1559,
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
when (index) {
|
||||
FEE_AMOUNT -> setOnAmountChange(value, index)
|
||||
MAX_FEE -> setOnMaxFeeChange(value, index)
|
||||
GAS_LIMIT -> setOnGasLimitChange(value, index)
|
||||
else -> set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<CustomFeeFieldUM>.setOnAmountChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(MAX_FEE)
|
||||
} else {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(GIGA_DECIMALS) // from ETH to GWEI
|
||||
|
||||
val newMaxFee = newFeeAmount.divide(gasLimit, this[MAX_FEE].decimals, RoundingMode.HALF_UP)
|
||||
|
||||
set(
|
||||
index = MAX_FEE,
|
||||
element = this[MAX_FEE].copy(value = newMaxFee.parseBigDecimal(this[MAX_FEE].decimals)),
|
||||
)
|
||||
|
||||
set(
|
||||
index = index,
|
||||
element = this[index].copy(
|
||||
value = value,
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatus.value.fiatRate,
|
||||
value = newFeeAmountDecimal,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<CustomFeeFieldUM>.setOnMaxFeeChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(MAX_FEE)
|
||||
} else {
|
||||
val newMaxFee = value.parseToBigDecimal(this[MAX_FEE].decimals).movePointLeft(this[MAX_FEE].decimals)
|
||||
val newFeeAmount = gasLimit * newMaxFee
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<CustomFeeFieldUM>.setOnGasLimitChange(value: String, index: Int) {
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_LIMIT)
|
||||
} else {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
|
||||
val maxFee = this[MAX_FEE].value.parseToBigDecimal(this[MAX_FEE].decimals)
|
||||
.movePointLeft(this[MAX_FEE].decimals) // from GWEI to ETH
|
||||
|
||||
val newFeeAmount = newGasLimit * maxFee
|
||||
|
||||
set(
|
||||
index = FEE_AMOUNT,
|
||||
element = this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val isNotExceedBalance = checkExceedBalance(
|
||||
feeBalance = currencyStatus.amount,
|
||||
feeAmount = newFeeAmount,
|
||||
)
|
||||
|
||||
set(
|
||||
index = index,
|
||||
element = this[index].copy(
|
||||
value = value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_FEE = 1
|
||||
const val PRIORITY_FEE = 2
|
||||
const val GAS_LIMIT = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
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
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class EthereumLegacyCustomFeeConverter(
|
||||
private val clickIntents: SendFeeClickIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : BaseEthereumCustomFeeConverter<Fee.Ethereum.Legacy> {
|
||||
|
||||
private val currencyStatus = feeCryptoCurrencyStatus.value
|
||||
|
||||
override fun convert(value: Fee.Ethereum.Legacy): ImmutableList<CustomFeeFieldUM> {
|
||||
return persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(
|
||||
normalFee: Fee.Ethereum.Legacy,
|
||||
value: ImmutableList<CustomFeeFieldUM>,
|
||||
): Fee.Ethereum.Legacy {
|
||||
val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals)
|
||||
val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
gasPrice = gasPrice,
|
||||
gasLimit = gasLimit,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum.Legacy): Int = GAS_LIMIT
|
||||
|
||||
override fun onValueChange(
|
||||
feeValue: Fee.Ethereum.Legacy,
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
when (index) {
|
||||
FEE_AMOUNT -> setOnAmountChange(value, index)
|
||||
GAS_PRICE -> setOnGasPriceChange(value, index)
|
||||
GAS_LIMIT -> setOnGasLimitChange(value, index)
|
||||
else -> set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<CustomFeeFieldUM>.setOnAmountChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(this[GAS_PRICE].decimals) // from ETH to GWEI
|
||||
val newGasPrice = newFeeAmount.divide(gasLimit, this[GAS_PRICE].decimals, RoundingMode.HALF_UP)
|
||||
set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(this[GAS_PRICE].decimals)))
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = newFeeAmountDecimal,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<CustomFeeFieldUM>.setOnGasPriceChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH
|
||||
val newFeeAmount = gasLimit * newGasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<CustomFeeFieldUM>.setOnGasLimitChange(value: String, index: Int) {
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_LIMIT)
|
||||
} else {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH
|
||||
|
||||
val newFeeAmount = newGasLimit * gasPrice
|
||||
|
||||
set(
|
||||
index = FEE_AMOUNT,
|
||||
element = this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val isNotExceedBalance = checkExceedBalance(
|
||||
feeBalance = currencyStatus.amount,
|
||||
feeAmount = newFeeAmount,
|
||||
)
|
||||
|
||||
set(
|
||||
index = index,
|
||||
element = this[index].copy(
|
||||
value = value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val GAS_PRICE = 1
|
||||
const val GAS_LIMIT = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class KaspaCustomFeeConverter(
|
||||
private val clickIntents: SendFeeClickIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : CustomFeeConverter<Fee.Kaspa> {
|
||||
|
||||
private val currencyStatus = feeCryptoCurrencyStatus.value
|
||||
|
||||
override fun convert(value: Fee.Kaspa): ImmutableList<CustomFeeFieldUM> {
|
||||
val feeValue = value.amount.value
|
||||
return persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Companion.Next,
|
||||
keyboardType = KeyboardType.Companion.Number,
|
||||
),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_amount_fee_footer),
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = feeValue,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Kaspa, value: ImmutableList<CustomFeeFieldUM>): Fee.Kaspa {
|
||||
val decimals = value[FEE_AMOUNT_INDEX].decimals
|
||||
val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(decimals)
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
mass = normalFee.mass,
|
||||
feeRate = feeAmount
|
||||
.divide(normalFee.mass.toBigDecimal(), decimals, RoundingMode.HALF_UP)
|
||||
.movePointRight(decimals)
|
||||
.toBigInteger(),
|
||||
)
|
||||
}
|
||||
|
||||
fun onValueChange(
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
when (index) {
|
||||
FEE_AMOUNT_INDEX -> {
|
||||
val valueDecimal = value.parseToBigDecimal(this[FEE_AMOUNT_INDEX].decimals)
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = valueDecimal,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun tryAutoFixValue(
|
||||
minimumFee: Fee.Kaspa,
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
val minimumFeeAmountValue = minimumFee.amount.value
|
||||
|
||||
return mutableCustomValues.apply {
|
||||
// check that there is reveal transaction info (= krc-20 token transfer)
|
||||
// return without changes otherwise
|
||||
if (minimumFee.revealTransactionFee != null && minimumFeeAmountValue != null) {
|
||||
getOrNull(FEE_AMOUNT_INDEX)?.let {
|
||||
val valueDecimal = it.value.parseToBigDecimal(it.decimals)
|
||||
// krc-20 transaction will be failed if custom fee value is less than minimum,
|
||||
// so we set value to minimum in this case
|
||||
if (valueDecimal < minimumFee.amount.value) {
|
||||
val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals)
|
||||
set(
|
||||
FEE_AMOUNT_INDEX,
|
||||
it.copy(
|
||||
value = fixedValue,
|
||||
label = getFiatReference(
|
||||
rate = currencyStatus.fiatRate,
|
||||
value = valueDecimal,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val FEE_AMOUNT_INDEX = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.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.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.fee.model.converters.SendFeeCustomFieldConverter
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.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.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.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.id.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.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.features.send.v2.subcomponents.fee.model.converters.FeeConverter
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
FeeSelectorUM.Content(
|
||||
fees = fees,
|
||||
customValues = feeSelectorUM.customValues,
|
||||
selectedType = feeSelectorUM.selectedType,
|
||||
selectedFee = feeConverter.convert(
|
||||
FeeConverter.Data(
|
||||
feeType = feeSelectorUM.selectedType,
|
||||
customValues = feeSelectorUM.customValues,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return state.copy(
|
||||
feeSelectorUM = updatedFeeSelector,
|
||||
isFeeApproximate = isFeeApproximate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.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.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
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.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.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
|
||||
|
||||
val backgroundColor = if (isEditingDisabled) {
|
||||
TangemTheme.colors.button.disabled
|
||||
} else {
|
||||
TangemTheme.colors.background.action
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(backgroundColor)
|
||||
.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(
|
||||
titleRes = title,
|
||||
iconRes = icon,
|
||||
preDot = stringReference(
|
||||
feeAmount?.value.format {
|
||||
crypto(
|
||||
symbol = feeAmount?.currencySymbol.orEmpty(),
|
||||
decimals = feeAmount?.decimals ?: 0,
|
||||
).fee(canBeLower = feeUM.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
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.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun SendCustomFee(
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
selectedFee: FeeType,
|
||||
hasNotifications: Boolean,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
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,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.customFee(
|
||||
feeSelectorUM: FeeSelectorUM.Content,
|
||||
hasNotifications: Boolean,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(key = FEE_CUSTOM_KEY) {
|
||||
SendCustomFee(
|
||||
customValues = feeSelectorUM.customValues,
|
||||
selectedFee = feeSelectorUM.selectedType,
|
||||
hasNotifications = hasNotifications,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(top = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
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.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(
|
||||
titleRes = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.subcomponents.fee.ui.state
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class CustomFeeFieldUM(
|
||||
val value: String,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val keyboardOptions: KeyboardOptions,
|
||||
val keyboardActions: KeyboardActions,
|
||||
val symbol: String?,
|
||||
val decimals: Int,
|
||||
val title: TextReference,
|
||||
val footer: TextReference,
|
||||
val label: TextReference? = null,
|
||||
val isReadonly: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Stable
|
||||
internal sealed class FeeSelectorUM {
|
||||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
val selectedType: FeeType = FeeType.Market,
|
||||
val selectedFee: Fee?,
|
||||
val customValues: ImmutableList<CustomFeeFieldUM> = persistentListOf(),
|
||||
) : 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@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 notifications: ImmutableList<NotificationUM>,
|
||||
val isEditingDisabled: Boolean = false,
|
||||
) : FeeUM()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : FeeUM()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue