Updated on 2026-08-14
This commit is contained in:
parent
5421e5eb69
commit
165bf516e6
256 changed files with 1098 additions and 1035 deletions
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
|
||||
interface FeeSelectorBlockComponent : ComposableContentComponent {
|
||||
|
||||
fun updateState(feeSelectorUM: FeeSelectorUM)
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
): FeeSelectorBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
|
||||
interface FeeSelectorComponent : ComposableBottomSheetComponent {
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
onDismiss: () -> Unit,
|
||||
): FeeSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface NFTSendComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val nftAsset: NFTAsset,
|
||||
val nftCollectionName: String,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, NFTSendComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableDialogComponent
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface NetworkSelectionComponent : ComposableDialogComponent {
|
||||
|
||||
data class Params(
|
||||
val address: String,
|
||||
val amount: BigDecimal?,
|
||||
val memo: String?,
|
||||
val walletGroups: List<WalletGroup>,
|
||||
val onTokenSelected: (UserWalletId, CryptoCurrency) -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
) {
|
||||
data class WalletGroup(
|
||||
val userWalletId: UserWalletId,
|
||||
val walletName: String,
|
||||
val accounts: List<AccountGroup>,
|
||||
)
|
||||
|
||||
data class AccountGroup(
|
||||
val accountId: AccountId,
|
||||
val accountName: AccountName,
|
||||
val currencies: List<CryptoCurrency>,
|
||||
val hiddenTokensCount: Int = 0,
|
||||
)
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, NetworkSelectionComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface SendComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val transactionId: String? = null,
|
||||
val amount: String? = null,
|
||||
val tag: String? = null,
|
||||
val destinationAddress: String? = null,
|
||||
val entryType: EntryType = EntryType.Manual,
|
||||
val callback: ModelCallback? = null,
|
||||
)
|
||||
|
||||
enum class EntryType {
|
||||
Manual,
|
||||
QR,
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendComponent>
|
||||
|
||||
interface ModelCallback {
|
||||
fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface SendEntryPointComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendEntryPointComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
interface SendFeatureToggles
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.send.api
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SendNotificationsComponent {
|
||||
|
||||
val state: StateFlow<ImmutableList<NotificationUM>>
|
||||
|
||||
fun LazyListScope.content(
|
||||
state: ImmutableList<NotificationUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
hasPaddingAbove: Boolean = false,
|
||||
isClickDisabled: Boolean = false,
|
||||
)
|
||||
|
||||
data class Params(
|
||||
val analyticsCategoryName: String,
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val notificationData: NotificationData,
|
||||
val callback: ModelCallback,
|
||||
) {
|
||||
data class NotificationData(
|
||||
val destinationAddress: String?,
|
||||
val memo: String?,
|
||||
val amountValue: BigDecimal,
|
||||
val reduceAmountBy: BigDecimal,
|
||||
val isIgnoreReduce: Boolean,
|
||||
val fee: Fee?,
|
||||
val feeError: GetFeeError?,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val isReduceAmountAvailable: Boolean = true,
|
||||
)
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendNotificationsComponent>
|
||||
|
||||
interface ModelCallback {
|
||||
fun onFeeReload()
|
||||
|
||||
fun onAmountReduceTo(reduceTo: BigDecimal) {}
|
||||
|
||||
fun onAmountReduceBy(reduceBy: BigDecimal, reduceByDiff: BigDecimal) {}
|
||||
|
||||
fun onAmountIgnore() {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.tangem.features.send.api.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
||||
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
|
||||
|
||||
/**
|
||||
* Send analytics
|
||||
*/
|
||||
sealed class CommonSendAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = category, event = event, params = params) {
|
||||
|
||||
/** Recipient address screen opened */
|
||||
data class AddressScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Address Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
),
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
/** Amount screen opened */
|
||||
data class AmountScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val type: SendEntryType = SendEntryType.Manual,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Amount Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
TYPE to type.analyticsName,
|
||||
),
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
/** Fee screen opened */
|
||||
data class FeeScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val blockchain: String,
|
||||
val token: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
),
|
||||
)
|
||||
|
||||
data class FeeSummaryScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val blockchain: String,
|
||||
val token: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Summary Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
),
|
||||
)
|
||||
|
||||
data class FeeTokenScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val availableTokens: String,
|
||||
val blockchain: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Token Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
"Available Fee" to availableTokens,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
/** Confirmation screen opened */
|
||||
data class ConfirmationScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val fromDerivationIndex: Int?,
|
||||
val toDerivationIndex: Int?,
|
||||
val sendBlockchain: String,
|
||||
val sendToken: String,
|
||||
val type: SendEntryType = SendEntryType.Manual,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Confirm Screen Opened",
|
||||
params = buildMap {
|
||||
put(SOURCE, source.analyticsName)
|
||||
put("Token", sendToken)
|
||||
put("Blockchain", sendBlockchain)
|
||||
if (fromDerivationIndex != null || toDerivationIndex != null) {
|
||||
put(
|
||||
"Account Derivation From or To (optional)",
|
||||
"$fromDerivationIndex, $toDerivationIndex",
|
||||
)
|
||||
}
|
||||
put(TYPE, type.analyticsName)
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
/** If transaction delays notification is present */
|
||||
data class NoticeTransactionDelays(
|
||||
val categoryName: String,
|
||||
val token: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Notice - Transaction Delays Are Possible",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
|
||||
/** If error occurs during send transactions */
|
||||
data class TransactionError(
|
||||
val categoryName: String,
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
val errorCode: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Error - Transaction Rejected",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
ERROR_CODE to errorCode,
|
||||
),
|
||||
)
|
||||
|
||||
/** Close button clicked */
|
||||
data class CloseButtonClicked(
|
||||
val categoryName: String,
|
||||
val source: SendScreenSource,
|
||||
val isFromSummary: Boolean,
|
||||
val isValid: Boolean,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Button - Close",
|
||||
params = mapOf(
|
||||
SOURCE to source.name,
|
||||
"FromSummary" to if (isFromSummary) "Yes" else "No",
|
||||
"isValid" to if (isValid) "Yes" else "No",
|
||||
),
|
||||
)
|
||||
|
||||
/** Share button clicked */
|
||||
data class ShareButtonClicked(
|
||||
val categoryName: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Button - Share",
|
||||
)
|
||||
|
||||
/** Expore button clicked */
|
||||
data class ExploreButtonClicked(
|
||||
val categoryName: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Button - Explore",
|
||||
)
|
||||
|
||||
/** Screen reopened from confirmation screen */
|
||||
data class ScreenReopened(
|
||||
val categoryName: String,
|
||||
val source: SendScreenSource,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Screen Reopened",
|
||||
params = mapOf(SOURCE to source.name),
|
||||
)
|
||||
|
||||
/** Fee screen is closed with non empty nonce */
|
||||
data class NonceInserted(
|
||||
val categoryName: String,
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Nonce Inserted",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
/** Token chosen to convert with sending */
|
||||
data class TokenChosen(
|
||||
val categoryName: String,
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Token chosen",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val SEND_CATEGORY = "Token / Send"
|
||||
const val SWAP_CATEGORY = "Swap"
|
||||
const val NFT_SEND_CATEGORY = "NFT"
|
||||
const val APPROVE_CATEGORY = "Approve"
|
||||
}
|
||||
|
||||
enum class SendScreenSource {
|
||||
Address,
|
||||
Amount,
|
||||
Fee,
|
||||
Confirm,
|
||||
}
|
||||
|
||||
enum class SendEntryType(val analyticsName: String) {
|
||||
QR("QR"),
|
||||
Manual("Manually"),
|
||||
}
|
||||
|
||||
enum class CommonSendSource(val analyticsName: String) {
|
||||
Send("Send"),
|
||||
Swap("Swap"),
|
||||
SendWithSwap("Send&Swap"),
|
||||
WalletConnect("WalletConnect"),
|
||||
NFT("NFT"),
|
||||
Approve("Approve"),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.send.api.callbacks
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
|
||||
interface FeeSelectorModelCallback {
|
||||
fun onFeeResult(feeSelectorUM: FeeSelectorUM)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.send.api.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface SellRedirectDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): SellRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.api.entity
|
||||
|
||||
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
|
||||
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,130 @@
|
|||
package com.tangem.features.send.api.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.api.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@Immutable
|
||||
sealed class FeeSelectorUM {
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
data object Loading : FeeSelectorUM() {
|
||||
override val isPrimaryButtonEnabled = false
|
||||
}
|
||||
|
||||
data class Error(
|
||||
val error: GetFeeError,
|
||||
val isHidden: Boolean = false,
|
||||
) : FeeSelectorUM() {
|
||||
override val isPrimaryButtonEnabled = false
|
||||
}
|
||||
|
||||
data class Content(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val fees: TransactionFee,
|
||||
val feeItems: ImmutableList<FeeItem>,
|
||||
val selectedFeeItem: FeeItem,
|
||||
val feeExtraInfo: FeeExtraInfo,
|
||||
val feeFiatRateUM: FeeFiatRateUM?,
|
||||
val feeNonce: FeeNonce,
|
||||
) : FeeSelectorUM() {
|
||||
fun toAnalyticType(): AnalyticsParam.FeeType = when (fees) {
|
||||
is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed
|
||||
is TransactionFee.Choosable -> when (selectedFeeItem) {
|
||||
is FeeItem.Suggested,
|
||||
is FeeItem.Custom,
|
||||
-> AnalyticsParam.FeeType.Custom
|
||||
is FeeItem.Fast -> AnalyticsParam.FeeType.Max
|
||||
is FeeItem.Market, is FeeItem.Loading -> AnalyticsParam.FeeType.Normal
|
||||
is FeeItem.Slow -> AnalyticsParam.FeeType.Min
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class FeeFiatRateUM(
|
||||
val rate: BigDecimal,
|
||||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class FeeExtraInfo(
|
||||
val isFeeApproximate: Boolean,
|
||||
val isFeeConvertibleToFiat: Boolean,
|
||||
val isTronToken: Boolean,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val availableFeeCurrencies: ImmutableList<CryptoCurrencyStatus>? = null,
|
||||
val transactionFeeExtended: TransactionFeeExtended? = null,
|
||||
val isNotEnoughFunds: Boolean = false,
|
||||
)
|
||||
|
||||
sealed class FeeNonce {
|
||||
data object None : FeeNonce()
|
||||
data class Nonce(
|
||||
val nonce: BigInteger?,
|
||||
val onNonceChange: (String) -> Unit,
|
||||
) : FeeNonce()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class FeeItem {
|
||||
abstract val fee: Fee
|
||||
abstract val title: TextReference
|
||||
abstract val iconRes: Int
|
||||
|
||||
fun isSameClass(other: FeeItem): Boolean {
|
||||
return this::class == other::class
|
||||
}
|
||||
|
||||
fun isLoading(): Boolean {
|
||||
return this is Loading
|
||||
}
|
||||
|
||||
object Loading : FeeItem() {
|
||||
override val title: TextReference = resourceReference(R.string.common_fee_selector_option_market)
|
||||
override val iconRes: Int = R.drawable.ic_bird_24
|
||||
override val fee: Fee = Fee.Common(Amount(BigDecimal.ZERO, Blockchain.Unknown))
|
||||
}
|
||||
|
||||
data class Suggested(
|
||||
override val title: TextReference,
|
||||
override val fee: Fee,
|
||||
) : FeeItem() {
|
||||
override val iconRes: Int = R.drawable.ic_star_mini_24
|
||||
}
|
||||
|
||||
data class Slow(override val fee: Fee) : FeeItem() {
|
||||
override val title: TextReference = resourceReference(R.string.common_fee_selector_option_slow)
|
||||
override val iconRes: Int = R.drawable.ic_tortoise_24
|
||||
}
|
||||
|
||||
data class Market(override val fee: Fee) : FeeItem() {
|
||||
override val title: TextReference = resourceReference(R.string.common_fee_selector_option_market)
|
||||
override val iconRes: Int = R.drawable.ic_bird_24
|
||||
}
|
||||
|
||||
data class Fast(override val fee: Fee) : FeeItem() {
|
||||
override val title: TextReference = resourceReference(R.string.common_fee_selector_option_fast)
|
||||
override val iconRes: Int = R.drawable.ic_hare_24
|
||||
}
|
||||
|
||||
data class Custom(override val fee: Fee, val customValues: ImmutableList<CustomFeeFieldUM>) : FeeItem() {
|
||||
override val title: TextReference = resourceReference(R.string.common_custom)
|
||||
override val iconRes: Int = R.drawable.ic_edit_v2_24
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.send.api.entity
|
||||
|
||||
sealed class PredefinedValues {
|
||||
data object Empty : PredefinedValues()
|
||||
|
||||
sealed class Content : PredefinedValues() {
|
||||
abstract val address: String
|
||||
abstract val amount: String?
|
||||
abstract val memo: String?
|
||||
|
||||
data class Deeplink(
|
||||
override val amount: String,
|
||||
override val address: String,
|
||||
override val memo: String?,
|
||||
val transactionId: String,
|
||||
) : Content()
|
||||
|
||||
data class QrCode(
|
||||
override val amount: String?,
|
||||
override val address: String,
|
||||
override val memo: String?,
|
||||
val source: Source,
|
||||
) : Content()
|
||||
}
|
||||
|
||||
enum class Source {
|
||||
MAIN_SCREEN,
|
||||
SEND_SCREEN,
|
||||
}
|
||||
}
|
||||
|
||||
inline val PredefinedValues.isFromMainScreenQr: Boolean
|
||||
get() = (this as? PredefinedValues.Content.QrCode)
|
||||
?.source == PredefinedValues.Source.MAIN_SCREEN
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.send.api.entry
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
|
||||
/**
|
||||
* Route for switching send and send via swap flows.
|
||||
*/
|
||||
sealed class SendEntryRoute : Route {
|
||||
|
||||
/** Route to send screen */
|
||||
data object Send : SendEntryRoute()
|
||||
/** Route to send via swap screen */
|
||||
data object SendWithSwap : SendEntryRoute()
|
||||
/** Route to choose token screen for send via swap */
|
||||
data class ChooseToken(
|
||||
val isShowSendViaSwapNotification: Boolean,
|
||||
) : SendEntryRoute()
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.features.send.api.params
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
|
||||
sealed class FeeSelectorParams {
|
||||
abstract val state: FeeSelectorUM
|
||||
abstract val userWalletId: UserWalletId
|
||||
abstract val onLoadFeeExtended: (suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>)?
|
||||
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
|
||||
abstract val onDisableCustomFee: () -> Boolean
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val feeStateConfiguration: FeeStateConfiguration
|
||||
abstract val feeDisplaySource: FeeDisplaySource
|
||||
abstract val analyticsCategoryName: String
|
||||
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
|
||||
abstract val shouldShowOnlySpeedOption: Boolean
|
||||
|
||||
data class FeeSelectorBlockParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val onLoadFeeExtended: (
|
||||
suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>
|
||||
)? = null,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val feeStateConfiguration: FeeStateConfiguration,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val shouldShowOnlySpeedOption: Boolean = false,
|
||||
override val onDisableCustomFee: () -> Boolean = { false },
|
||||
val bottomSheetShown: (Boolean) -> Unit = {},
|
||||
) : FeeSelectorParams()
|
||||
|
||||
data class FeeSelectorDetailsParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val onLoadFeeExtended: (
|
||||
suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>
|
||||
)? = null,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val feeStateConfiguration: FeeStateConfiguration,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val shouldShowOnlySpeedOption: Boolean = false,
|
||||
override val onDisableCustomFee: () -> Boolean = { false },
|
||||
val callback: FeeSelectorModelCallback,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
sealed class FeeStateConfiguration {
|
||||
data object None : FeeStateConfiguration()
|
||||
data class Suggestion(val title: TextReference, val fee: Fee) : FeeStateConfiguration()
|
||||
|
||||
data object ExcludeLow : FeeStateConfiguration()
|
||||
}
|
||||
|
||||
enum class FeeDisplaySource {
|
||||
Screen,
|
||||
BottomSheet,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.send.api.subcomponents.amount.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
|
||||
sealed class CommonSendAmountAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = category, event = event, params = params) {
|
||||
|
||||
/** Selected currency */
|
||||
data class SelectedCurrency(
|
||||
val categoryName: String,
|
||||
val type: SelectedCurrencyType,
|
||||
) : CommonSendAmountAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Selected Currency",
|
||||
params = mapOf(TYPE to type.value),
|
||||
)
|
||||
|
||||
/** Max amount button clicked */
|
||||
data class MaxAmountButtonClicked(
|
||||
val categoryName: String,
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
val source: CommonSendAnalyticEvents.CommonSendSource,
|
||||
) : CommonSendAmountAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Max Amount Taped",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
SOURCE to source.analyticsName,
|
||||
),
|
||||
)
|
||||
|
||||
enum class SelectedCurrencyType(val value: String) {
|
||||
Token("Token"),
|
||||
AppCurrency("App Currency"),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination
|
||||
|
||||
/**
|
||||
* Common route for destination
|
||||
*/
|
||||
interface DestinationRoute {
|
||||
val isEditMode: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
|
||||
interface SendDestinationBlockComponent : ComposableContentComponent {
|
||||
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendDestinationComponentParams.DestinationBlockParams,
|
||||
onClick: () -> Unit,
|
||||
onResult: (DestinationUM) -> Unit,
|
||||
): SendDestinationBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination
|
||||
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
|
||||
interface SendDestinationComponent : ComposableContentComponent {
|
||||
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface ModelCallback : NavigationModelCallback {
|
||||
fun onDestinationResult(destinationUM: DestinationUM)
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<SendDestinationComponentParams.DestinationParams, SendDestinationComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
sealed class SendDestinationComponentParams {
|
||||
|
||||
abstract val state: DestinationUM
|
||||
abstract val analyticsCategoryName: String
|
||||
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
|
||||
abstract val userWalletId: UserWalletId
|
||||
abstract val cryptoCurrency: CryptoCurrency
|
||||
abstract val isAllowSelfSend: Boolean
|
||||
|
||||
data class DestinationParams(
|
||||
override val state: DestinationUM,
|
||||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val cryptoCurrency: CryptoCurrency,
|
||||
override val userWalletId: UserWalletId,
|
||||
val title: TextReference,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val currentRoute: Flow<DestinationRoute>,
|
||||
val callback: SendDestinationComponent.ModelCallback,
|
||||
override val isAllowSelfSend: Boolean = false,
|
||||
) : SendDestinationComponentParams()
|
||||
|
||||
data class DestinationBlockParams(
|
||||
override val state: DestinationUM,
|
||||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val cryptoCurrency: CryptoCurrency,
|
||||
val blockClickEnableFlow: StateFlow<Boolean>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
override val isAllowSelfSend: Boolean = false,
|
||||
) : SendDestinationComponentParams()
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
@Immutable
|
||||
data class DestinationRecipientListUM(
|
||||
val id: String,
|
||||
val title: TextReference = TextReference.EMPTY,
|
||||
val subtitle: TextReference = TextReference.EMPTY,
|
||||
val accountTitleUM: AccountTitleUM.Account? = null,
|
||||
val timestamp: TextReference? = null,
|
||||
val subtitleEndOffset: Int = 0,
|
||||
@DrawableRes val subtitleIconRes: Int? = null,
|
||||
val isVisible: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
val userWalletId: UserWalletId? = null,
|
||||
val network: Network? = null,
|
||||
val accountId: AccountId? = null,
|
||||
val address: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination.entity
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
|
||||
@Immutable
|
||||
sealed class DestinationTextFieldUM {
|
||||
|
||||
/** Current value */
|
||||
abstract val value: String
|
||||
|
||||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
data class RecipientAddress(
|
||||
override val value: String,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val isValuePasted: Boolean,
|
||||
// if value is human-readable address, this field contains the actual blockchain address
|
||||
val blockchainAddress: String? = null,
|
||||
) : DestinationTextFieldUM() {
|
||||
|
||||
val actualAddress: String
|
||||
get() = blockchainAddress ?: value
|
||||
|
||||
// if value is human-readable address, this field contains the actual brief blockchain address
|
||||
val briefBlockchainAddress: String?
|
||||
get() = blockchainAddress?.toBriefAddressFormat(BRIEF_ADDRESS_EDGE_LENGTH, BRIEF_ADDRESS_EDGE_LENGTH)
|
||||
|
||||
val isAddressEns = blockchainAddress != null
|
||||
}
|
||||
|
||||
data class RecipientMemo(
|
||||
override val value: String,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val disabledText: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
val isValuePasted: Boolean,
|
||||
) : DestinationTextFieldUM()
|
||||
|
||||
private companion object {
|
||||
const val BRIEF_ADDRESS_EDGE_LENGTH = 13
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
sealed class DestinationUM {
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
data class Content(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val addressTextField: DestinationTextFieldUM.RecipientAddress,
|
||||
val memoTextField: DestinationTextFieldUM.RecipientMemo?,
|
||||
val recent: ImmutableList<DestinationRecipientListUM>,
|
||||
val wallets: ImmutableList<DestinationRecipientListUM>,
|
||||
val networkName: String,
|
||||
val isValidating: Boolean = false,
|
||||
val isInitialized: Boolean = false,
|
||||
val isRecentHidden: Boolean,
|
||||
val isAccountsMode: Boolean? = null,
|
||||
) : DestinationUM()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : DestinationUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.send.api.subcomponents.feeSelector
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface FeeSelectorCheckReloadTrigger {
|
||||
/** Trigger return callback with check result */
|
||||
suspend fun callbackCheckResult(isSuccess: Boolean)
|
||||
|
||||
/** Trigger fee check reload */
|
||||
suspend fun triggerCheckUpdate()
|
||||
}
|
||||
|
||||
interface FeeSelectorCheckReloadListener {
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.send.api.subcomponents.feeSelector
|
||||
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface FeeSelectorReloadTrigger {
|
||||
|
||||
/** Trigger fee update */
|
||||
suspend fun triggerUpdate(feeData: FeeSelectorData = FeeSelectorData())
|
||||
|
||||
/** Triggers fee loading status. Used in cases where fee loading state is extended due to business logic */
|
||||
suspend fun triggerLoadingState()
|
||||
}
|
||||
|
||||
interface FeeSelectorReloadListener {
|
||||
/** Flow triggers fee reload */
|
||||
val reloadTriggerFlow: Flow<FeeSelectorData>
|
||||
|
||||
/** Triggers fee loading status. Used in cases where fee loading state is extended due to business logic */
|
||||
val loadingStateTriggerFlow: Flow<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.send.api.subcomponents.feeSelector.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
|
||||
sealed class CommonSendFeeAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : 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,
|
||||
val source: CommonSendAnalyticEvents.CommonSendSource,
|
||||
val feeToken: String,
|
||||
val blockchain: String,
|
||||
) : CommonSendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Selected",
|
||||
params = mapOf(
|
||||
FEE_TYPE to feeType.value,
|
||||
SOURCE to source.analyticsName,
|
||||
FEE_TOKEN to feeToken,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
/** Custom fee selected */
|
||||
data class CustomFeeButtonClicked(
|
||||
override val categoryName: String,
|
||||
val blockchain: String,
|
||||
val token: String,
|
||||
) : CommonSendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Custom Fee Clicked",
|
||||
params = mapOf(
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
),
|
||||
)
|
||||
|
||||
/** Custom fee edited */
|
||||
data class GasPriceInserter(
|
||||
override val categoryName: String,
|
||||
) : CommonSendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Gas Price Inserted",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.send.api.subcomponents.feeSelector.entity
|
||||
|
||||
data class FeeSelectorData(
|
||||
val isRemoveSuggestedFee: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.features.send.api.subcomponents.feeSelector.utils
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
object FeeCalculationUtils {
|
||||
|
||||
private val FEE_MAX_DIFF = BigDecimal("5")
|
||||
|
||||
/**
|
||||
* Check and calculates subtracted amount
|
||||
*/
|
||||
fun checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal?,
|
||||
reduceAmountBy: BigDecimal,
|
||||
): BigDecimal {
|
||||
if (feeValue == null || feeValue.isZero()) return amountValue
|
||||
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
|
||||
*/
|
||||
fun checkIfCustomFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content): Pair<Boolean, String> {
|
||||
val defaultResult = false to ""
|
||||
|
||||
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return defaultResult
|
||||
|
||||
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return defaultResult
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return defaultResult
|
||||
val highValue = multipleFees.priority.amount.value ?: return defaultResult
|
||||
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = if (highValue > BigDecimal.ZERO) {
|
||||
customValue / highValue
|
||||
} else {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
val isFeeTooHigh = diff > FEE_MAX_DIFF
|
||||
return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too low
|
||||
*/
|
||||
fun checkIfCustomFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean {
|
||||
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return false
|
||||
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return false
|
||||
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
|
||||
return minimumValue > customValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sending amount with fee is greater than balance
|
||||
*/
|
||||
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,12 @@
|
|||
package com.tangem.features.send.api.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SendNotificationsUpdateListener {
|
||||
/** Flow triggers notifications update */
|
||||
val updateTriggerFlow: Flow<SendNotificationsComponent.Params.NotificationData>
|
||||
|
||||
/** Flow returns whether there is error notifications */
|
||||
val hasErrorFlow: Flow<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.send.api.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
|
||||
interface SendNotificationsUpdateTrigger {
|
||||
/** Trigger return callback with check result */
|
||||
suspend fun callbackHasError(hasError: Boolean)
|
||||
|
||||
/** Trigger fee check reload */
|
||||
suspend fun triggerUpdate(data: SendNotificationsComponent.Params.NotificationData)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.send.api.utils
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.api.R
|
||||
import com.tangem.utils.StringsSigns.COMA_SIGN
|
||||
|
||||
fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference {
|
||||
val suffix = when {
|
||||
fee.remainingEnergy == 0L -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_including,
|
||||
wrappedList(fiatFee),
|
||||
)
|
||||
}
|
||||
fee.feeEnergy <= fee.remainingEnergy -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_fee_covered,
|
||||
wrappedList(fee.feeEnergy),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_fee_reduced,
|
||||
wrappedList(fee.remainingEnergy),
|
||||
)
|
||||
}
|
||||
}
|
||||
val prefix = resourceReference(
|
||||
R.string.send_summary_transaction_description_prefix,
|
||||
wrappedList(fiatSending),
|
||||
)
|
||||
|
||||
return combinedReference(prefix, stringReference("$COMA_SIGN "), suffix)
|
||||
}
|
||||
|
||||
fun formatFooterFiatFee(
|
||||
amount: Amount?,
|
||||
isFeeConvertibleToFiat: Boolean,
|
||||
isFeeApproximate: Boolean,
|
||||
appCurrency: AppCurrency,
|
||||
): String {
|
||||
return if (isFeeConvertibleToFiat) {
|
||||
amount?.value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
amount?.value.format {
|
||||
crypto(
|
||||
decimals = amount?.decimals ?: 0,
|
||||
symbol = amount?.currencySymbol.orEmpty(),
|
||||
).fee(
|
||||
canBeLower = isFeeApproximate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package com.tangem.features.send.api.subcomponents.feeSelector.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class FeeCalculationUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount subtract available and fee coverage needed WHEN checkAndCalculateSubtractedAmount THEN returns subtracted amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("6"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(BigDecimal("3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount subtract not available WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = false
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sufficient balance for amount and fee WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no balance WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(null)
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee exceeds balance WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("5")
|
||||
val feeAmount = BigDecimal("10")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee within balance WHEN checkExceedBalance THEN returns false`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount = BigDecimal("5")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee amount WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount: BigDecimal? = null
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee balance WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance: BigDecimal? = null
|
||||
val feeAmount = BigDecimal("5")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero fee amount WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount = BigDecimal.ZERO
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
private fun createCryptoCurrencyStatus(amount: BigDecimal?): CryptoCurrencyStatus {
|
||||
val value = if (amount != null) {
|
||||
CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = BigDecimal.ZERO,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
stakingBalance = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
networkAddress = mockk(relaxed = true),
|
||||
yieldSupplyStatus = null,
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyStatus.NoAmount(
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
)
|
||||
}
|
||||
return CryptoCurrencyStatus(
|
||||
currency = mockk(relaxed = true),
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue