Updated on 2026-08-14
This commit is contained in:
commit
cbcb1dae0b
49 changed files with 490 additions and 242 deletions
|
|
@ -2,8 +2,8 @@ package com.tangem.tap.common.analytics.converters
|
|||
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||
|
|
@ -22,20 +22,24 @@ internal object TransactionDomainModule {
|
|||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade)
|
||||
return GetFeeUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig(),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSendTransactionUseCase(
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
demoConfig = DemoConfig(),
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
transactionRepository = transactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
|||
import com.tangem.tap.domain.TangemSigner
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class MoonPayService(
|
|||
.appendQueryParameter("apiKey", apiKey)
|
||||
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
|
||||
.appendQueryParameter("refundWalletAddress", walletAddress)
|
||||
.appendQueryParameter("redirectURL", "tangem://redirect_sell")
|
||||
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
|
||||
|
||||
if (isDarkTheme) uri.appendQueryParameter("theme", "dark")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
|
|||
transactionId = params["transactionId"] ?: return,
|
||||
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
|
||||
depositWalletAddress = params["depositWalletAddress"] ?: return,
|
||||
currencyId = params["currency_id"] ?: return,
|
||||
depositWalletAddressTag = params["depositWalletAddressTag"],
|
||||
)
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
|
|||
val transactionId: String,
|
||||
val baseCurrencyAmount: String,
|
||||
val depositWalletAddress: String,
|
||||
val currencyId: String,
|
||||
val depositWalletAddressTag: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
{
|
||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "5.9.0"
|
||||
"version": "5.9.1"
|
||||
},
|
||||
{
|
||||
"name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@
|
|||
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
|
||||
<string name="card_settings_security_mode">Тип безопасности</string>
|
||||
<string name="card_settings_title">Настройки карты</string>
|
||||
<string name="cardano_coin_will_be_send_with_token_description">Ввиду особенностей сети Cardano при транзакции токена %1$s помимо комиссии сети будет списано %2$s</string>
|
||||
<string name="cardano_insufficient_balance_to_send_token_description">Для совершения транзакции %1$s, вам необходимо внести немного %2$s (%3$s), чтобы покрыть комиссию сети и минимальное значение ADA для отправки.</string>
|
||||
<string name="cardano_insufficient_balance_to_send_token_title">Недостаточно ADA для отправки токена</string>
|
||||
<string name="cardano_max_amount_has_token_description">Вывод всего баланса ADA невозможен при наличии средств на токенах сети Cardano. Сначала выведите средства на ваших токенах.</string>
|
||||
<string name="common_accept">Принять</string>
|
||||
<string name="common_access_denied">Доступ запрещен</string>
|
||||
<string name="common_apply">Применить</string>
|
||||
|
|
@ -96,7 +100,8 @@
|
|||
<string name="common_no_address">Нет адреса</string>
|
||||
<string name="common_ok">OK</string>
|
||||
<string name="common_origin_card">Основная карта</string>
|
||||
<string name="common_passphrase">Кодовая фраза</string>
|
||||
<string name="common_passphrase">Парольная фраза</string>
|
||||
<string name="common_paste">Вставить</string>
|
||||
<string name="common_read_more">Подробнее</string>
|
||||
<string name="common_receive">Получить</string>
|
||||
<string name="common_reject">Отклонить</string>
|
||||
|
|
@ -183,9 +188,9 @@
|
|||
<string name="express_choose_providers_subtitle">Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами</string>
|
||||
<string name="express_choose_providers_title">Выберите провайдера</string>
|
||||
<string name="express_error_code">Произошла ошибка. Код: %s</string>
|
||||
<string name="express_error_provider_unavailable">К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код: %s)</string>
|
||||
<string name="express_error_provider_unavailable">К сожалению, обмен указанной пары через выбранного провайдера на данный момент невозможен. Попробуйте совершить обмен позже. (Код: %s)</string>
|
||||
<string name="express_error_swap_pair_unavailable">Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s)</string>
|
||||
<string name="express_error_swap_unavailable">В данный момент обмен не возможен. Попробуйте позже. (Код: %s)</string>
|
||||
<string name="express_error_swap_unavailable">В данный момент обмен невозможен. Попробуйте позже. (Код: %s)</string>
|
||||
<string name="express_estimated_amount">Курс обмена</string>
|
||||
<string name="express_exchange_by">Обмен через %s</string>
|
||||
<string name="express_exchange_notification_failed_text">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
|
||||
|
|
@ -288,7 +293,7 @@
|
|||
<string name="onboarding_activation_error_title">Ошибка активации</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
|
||||
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
|
||||
<string name="onboarding_bottom_sheet_passphrase_description">Кодовая фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов.</string>
|
||||
<string name="onboarding_bottom_sheet_passphrase_description">Парольная фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов.</string>
|
||||
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
|
||||
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
|
||||
<string name="onboarding_button_backup_now">Создать резервную копию</string>
|
||||
|
|
@ -478,7 +483,7 @@
|
|||
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
|
||||
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||
<string name="send_notification_transaction_limit_title">Лимит транзакции</string>
|
||||
<string name="send_optional_field">Необязательное</string>
|
||||
<string name="send_optional_field">Опционально</string>
|
||||
<string name="send_qrcode_scan_info">Пожалуйста, совместите свой QR-код с квадратом, чтобы отсканировать его. Убедитесь, что вы сканируете адрес в сети %s.</string>
|
||||
<string name="send_recent_transactions">Последние</string>
|
||||
<string name="send_recipient">Получатель</string>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@
|
|||
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
|
||||
<string name="card_settings_security_mode">Security Mode</string>
|
||||
<string name="card_settings_title">Card Settings</string>
|
||||
<string name="cardano_coin_will_be_send_with_token_description">Due to the peculiarities of the Cardano network, when transacting the %1$s token, in addition to the network commission, %2$s will be charged</string>
|
||||
<string name="cardano_insufficient_balance_to_send_token_description">To make a %1$s transaction, you must deposit some %2$s (%3$s) to cover the network fee and minimum ADA value</string>
|
||||
<string name="cardano_insufficient_balance_to_send_token_title">Insufficient ADA to token transfer</string>
|
||||
<string name="cardano_max_amount_has_token_description">Withdrawal of the entire ADA balance is not possible if funds are available in Cardano network tokens. First, withdraw funds on your tokens.</string>
|
||||
<string name="common_accept">Accept</string>
|
||||
<string name="common_access_denied">Access denied</string>
|
||||
<string name="common_apply">Apply</string>
|
||||
|
|
@ -96,6 +100,7 @@
|
|||
<string name="common_ok">OK</string>
|
||||
<string name="common_origin_card">Primary Card</string>
|
||||
<string name="common_passphrase">Passphrase</string>
|
||||
<string name="common_paste">Paste</string>
|
||||
<string name="common_read_more">Read more</string>
|
||||
<string name="common_receive">Receive</string>
|
||||
<string name="common_reject">Reject</string>
|
||||
|
|
@ -427,6 +432,8 @@
|
|||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_base_fee">Base fee</string>
|
||||
<string name="send_base_fee_footer">Represents the part of the transaction fee that goes to the miner</string>
|
||||
<string name="send_date_format">%1$s, %2$s</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
<string name="send_destination_tag_field">Destination Tag</string>
|
||||
|
|
@ -474,6 +481,8 @@
|
|||
<string name="send_notification_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
|
||||
<string name="send_notification_transaction_limit_title">Transaction limitation</string>
|
||||
<string name="send_optional_field">Optional</string>
|
||||
<string name="send_priority_fee">Priority fee</string>
|
||||
<string name="send_priority_fee_footer">Represents the minimum gasUsed multiplier required for a transaction to be included in a block. This is the part of the transaction fee that is burnt.</string>
|
||||
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
|
||||
<string name="send_recent_transactions">Recent</string>
|
||||
<string name="send_recipient">Recipient</string>
|
||||
|
|
|
|||
|
|
@ -47,14 +47,15 @@ inline fun <reified T> SegmentedButtons(
|
|||
selectedColor: Color = TangemTheme.colors.background.action,
|
||||
dividerColor: Color = TangemTheme.colors.stroke.primary,
|
||||
showIndication: Boolean = true,
|
||||
selectedItem: T? = null,
|
||||
initialSelectedItem: T? = null,
|
||||
isEnabled: Boolean = true,
|
||||
crossinline buttonContent: @Composable (T) -> Unit,
|
||||
) {
|
||||
if (config.isEmpty() || config.size == 1) return
|
||||
|
||||
var selected by remember {
|
||||
val selectedIndex = if (selectedItem == null) 0 else config.indexOf(selectedItem)
|
||||
mutableIntStateOf(selectedIndex)
|
||||
var selectedIndex by remember {
|
||||
val index = if (initialSelectedItem == null) 0 else config.indexOf(initialSelectedItem)
|
||||
mutableIntStateOf(index)
|
||||
}
|
||||
|
||||
Row(
|
||||
|
|
@ -69,7 +70,7 @@ inline fun <reified T> SegmentedButtons(
|
|||
val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
|
||||
|
||||
val animateColor by animateColorAsState(
|
||||
targetValue = if (index == selected) selectedColor else color,
|
||||
targetValue = if (index == selectedIndex) selectedColor else color,
|
||||
label = "Segmented Button Selected Color Animation",
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMedium),
|
||||
)
|
||||
|
|
@ -86,11 +87,12 @@ inline fun <reified T> SegmentedButtons(
|
|||
),
|
||||
)
|
||||
.clickable(
|
||||
enabled = isEnabled,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (showIndication) LocalIndication.current else null,
|
||||
) {
|
||||
selectedIndex = index
|
||||
onClick(config[index])
|
||||
selected = index
|
||||
},
|
||||
) {
|
||||
buttonContent.invoke(config[index])
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||
|
||||
private const val GRAY_SCALE_SATURATION = 0f
|
||||
private const val GRAY_SCALE_ALPHA = 0.4f
|
||||
|
||||
/**
|
||||
* Simple icon from network
|
||||
*
|
||||
|
|
@ -20,16 +25,19 @@ import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
|||
fun FiatIcon(
|
||||
url: String?,
|
||||
size: Dp,
|
||||
isGrayscale: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
|
||||
) {
|
||||
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
|
||||
val alpha = if (isGrayscale) GRAY_SCALE_ALPHA else 1f
|
||||
val colorFilter = if (isGrayscale) GrayscaleColorFilter else null
|
||||
|
||||
DefaultCurrencyIcon(
|
||||
iconData = iconData,
|
||||
size = size,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
errorIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = fallbackResId),
|
||||
|
|
@ -38,4 +46,7 @@ fun FiatIcon(
|
|||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val GrayscaleColorFilter: ColorFilter
|
||||
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
|
||||
|
|
@ -20,6 +20,26 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
|
|||
}
|
||||
}
|
||||
|
||||
fun convertCustom(
|
||||
value: CryptoCurrencyStatus,
|
||||
forceGrayscale: Boolean,
|
||||
showCustomTokenBadge: Boolean,
|
||||
): TokenIconState {
|
||||
return when (val currency = value.currency) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(
|
||||
coin = currency,
|
||||
isUnreachable = value.value.isError,
|
||||
forceGrayscale = forceGrayscale,
|
||||
)
|
||||
is CryptoCurrency.Token -> getIconStateForToken(
|
||||
token = currency,
|
||||
isErrorStatus = value.value.isError,
|
||||
forceGrayscale = forceGrayscale,
|
||||
showCustomBadge = showCustomTokenBadge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun convert(currency: CryptoCurrency): TokenIconState {
|
||||
return when (currency) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(currency, isUnreachable = false)
|
||||
|
|
@ -27,18 +47,27 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
|
|||
}
|
||||
}
|
||||
|
||||
private fun getIconStateForCoin(coin: CryptoCurrency.Coin, isUnreachable: Boolean): TokenIconState.CoinIcon {
|
||||
private fun getIconStateForCoin(
|
||||
coin: CryptoCurrency.Coin,
|
||||
isUnreachable: Boolean,
|
||||
forceGrayscale: Boolean = false,
|
||||
): TokenIconState.CoinIcon {
|
||||
return TokenIconState.CoinIcon(
|
||||
url = coin.iconUrl,
|
||||
fallbackResId = coin.networkIconResId,
|
||||
isGrayscale = coin.network.isTestnet || isUnreachable,
|
||||
isGrayscale = forceGrayscale || coin.network.isTestnet || isUnreachable,
|
||||
showCustomBadge = coin.isCustom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenIconState {
|
||||
val isGrayscale = token.network.isTestnet || isErrorStatus
|
||||
val background = token.tryGetBackgroundForTokenIcon(isGrayscale)
|
||||
private fun getIconStateForToken(
|
||||
token: CryptoCurrency.Token,
|
||||
isErrorStatus: Boolean,
|
||||
showCustomBadge: Boolean = true,
|
||||
forceGrayscale: Boolean = false,
|
||||
): TokenIconState {
|
||||
val grayScale = forceGrayscale || token.network.isTestnet || isErrorStatus
|
||||
val background = token.tryGetBackgroundForTokenIcon(grayScale)
|
||||
val tint = getTintForTokenIcon(background)
|
||||
|
||||
return if (token.isCustom && token.iconUrl == null) {
|
||||
|
|
@ -46,16 +75,16 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
|
|||
tint = tint,
|
||||
background = background,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
isGrayscale = grayScale,
|
||||
)
|
||||
} else {
|
||||
TokenIconState.TokenIcon(
|
||||
url = token.iconUrl,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
isGrayscale = grayScale,
|
||||
fallbackTint = tint,
|
||||
fallbackBackground = background,
|
||||
showCustomBadge = token.isCustom, // `true` for tokens with custom derivation
|
||||
showCustomBadge = token.isCustom && showCustomBadge, // `true` for tokens with custom derivation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
|
|||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "Paste",
|
||||
text = stringResource(R.string.common_paste),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
|
||||
/**
|
||||
|
|
@ -47,7 +47,11 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie
|
|||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()),
|
||||
text = stringResource(
|
||||
id = R.string.send_date_format,
|
||||
date.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||
date.toTimeFormat(),
|
||||
),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -40,6 +40,29 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = cryptoCurrency.decimals
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.symbol.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009${cryptoCurrency.symbol}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import org.joda.time.format.DateTimeFormatter
|
|||
* If [this] timestamp is today or yesterday, returns relative date,
|
||||
* otherwise returns formatting date.
|
||||
*/
|
||||
fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
|
||||
fun Long.toDateFormatWithTodayYesterday(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
|
||||
val localDate = DateTime(this, DateTimeZone.getDefault())
|
||||
return if (localDate.isToday() || localDate.isYesterday()) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.features.demo
|
||||
package com.tangem.domain.demo
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
|
|
@ -184,8 +184,9 @@ sealed class CryptoCurrency : Parcelable {
|
|||
}
|
||||
|
||||
private companion object {
|
||||
// should use delimiters that could be used in URL not like path or query delimiters
|
||||
const val PREFIX_DELIMITER = '_'
|
||||
const val SUFFIX_DELIMITER = '#'
|
||||
const val SUFFIX_DELIMITER = ';'
|
||||
const val DERIVATION_PATH_DELIMITER = 'd'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
|
||||
sealed class SendTransactionError {
|
||||
|
||||
object DemoCardError : SendTransactionError()
|
||||
data object DemoCardError : SendTransactionError()
|
||||
|
||||
data class DataError(val message: String?) : SendTransactionError()
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ sealed class SendTransactionError {
|
|||
|
||||
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
||||
|
||||
object UserCancelledError : SendTransactionError()
|
||||
data object UserCancelledError : SendTransactionError()
|
||||
|
||||
data class CreateAccountUnderfunded(val amount: String) : SendTransactionError()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import com.tangem.blockchain.common.Amount
|
|||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -17,23 +19,31 @@ import java.math.BigDecimal
|
|||
*/
|
||||
class GetFeeUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val demoConfig: DemoConfig,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
amount: BigDecimal,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
) = either {
|
||||
catch(
|
||||
block = {
|
||||
val result = requireNotNull(
|
||||
walletManagersFacade.getFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount)
|
||||
|
||||
val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) {
|
||||
demoTransactionSender(userWallet, cryptoCurrency).getFee(
|
||||
amount = amountData,
|
||||
destination = destination,
|
||||
userWalletId = userWalletId,
|
||||
)
|
||||
} else {
|
||||
walletManagersFacade.getFee(
|
||||
amount = amountData,
|
||||
destination = destination,
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
),
|
||||
) { "Fee is null" }
|
||||
) ?: error("Fee is null")
|
||||
}
|
||||
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data
|
||||
|
|
@ -47,6 +57,17 @@ class GetFeeUseCase(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun demoTransactionSender(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): DemoTransactionSender {
|
||||
return DemoTransactionSender(
|
||||
walletManagersFacade
|
||||
.getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network)
|
||||
?: error("WalletManager is null"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = amount,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import arrow.core.left
|
|||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.ResultChecker
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
|
|
@ -12,20 +13,23 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.R
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
import com.tangem.utils.toFormattedString
|
||||
|
||||
class SendTransactionUseCase(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val demoConfig: DemoConfig,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val transactionRepository: TransactionRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
txData: TransactionData,
|
||||
|
|
@ -39,8 +43,13 @@ class SendTransactionUseCase(
|
|||
cardSdkConfigRepository.setLinkedTerminal(false)
|
||||
}
|
||||
val sendResult = try {
|
||||
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
|
||||
SendTransactionError.DemoCardError.left()
|
||||
if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) {
|
||||
sendDemo(
|
||||
userWallet = userWallet,
|
||||
network = network,
|
||||
transactionData = txData,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
transactionRepository.sendTransaction(
|
||||
txData = txData,
|
||||
|
|
@ -66,6 +75,27 @@ class SendTransactionUseCase(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun sendDemo(
|
||||
userWallet: UserWallet,
|
||||
network: Network,
|
||||
transactionData: TransactionData,
|
||||
signer: TransactionSigner,
|
||||
): Either<SendTransactionError, SimpleResult> {
|
||||
val demoTransactionSender = DemoTransactionSender(
|
||||
walletManagersFacade
|
||||
.getOrCreateWalletManager(userWallet.walletId, network)
|
||||
?: error("WalletManager is null"),
|
||||
)
|
||||
|
||||
val result = demoTransactionSender.send(transactionData = transactionData, signer = signer)
|
||||
|
||||
return if (result is SimpleResult.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) {
|
||||
SendTransactionError.DemoCardError.left()
|
||||
} else {
|
||||
result.right()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||
if (ResultChecker.isNetworkError(result)) {
|
||||
return SendTransactionError.NetworkError(
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modif
|
|||
SegmentedButtons(
|
||||
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
|
||||
config = state.seedSegments,
|
||||
selectedItem = state.selectedSeedType,
|
||||
initialSelectedItem = state.selectedSeedType,
|
||||
onClick = {
|
||||
state.onSelectType(it)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ internal class SendStateFactory(
|
|||
recipientState = state.recipientState
|
||||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
sendState = confirmStateConverter.convert(Unit),
|
||||
isEditingDisabled = true,
|
||||
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ internal sealed class SendStates {
|
|||
val walletBalance: TextReference,
|
||||
val tokenIconState: TokenIconState,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val isSegmentedButtonsEnabled: Boolean,
|
||||
val amountTextField: SendTextField.AmountField,
|
||||
val notifications: ImmutableList<SendNotification>,
|
||||
val appCurrencyCode: String,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ internal class SendAmountStateConverter(
|
|||
val status = cryptoCurrencyStatusProvider()
|
||||
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
||||
val noFeeRate = status.value.fiatRate.isNullOrZero()
|
||||
|
||||
return SendStates.AmountState(
|
||||
walletName = userWallet.name,
|
||||
|
|
@ -42,22 +43,23 @@ internal class SendAmountStateConverter(
|
|||
isFeeLoading = false,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
subtractedFee = null,
|
||||
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = false,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convertCustom(
|
||||
value = status,
|
||||
forceGrayscale = noFeeRate,
|
||||
showCustomTokenBadge = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconUrl = appCurrency.iconSmallUrl,
|
||||
isFiat = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconUrl = appCurrency.iconSmallUrl,
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
isSegmentedButtonsEnabled = !noFeeRate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,11 +19,13 @@ internal class SendAmountSubtractConverter(
|
|||
val feeValue = feeState.fee?.amount?.value ?: return state
|
||||
val amountTextField = amountState.amountTextField
|
||||
val amountValue = amountTextField.cryptoAmount.value ?: return state
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return state
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
|
||||
val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) } ?: feeValue
|
||||
val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) }
|
||||
?: amountValue.minus(balance.minus(feeValue))
|
||||
val decimalCryptoValue = amountValue.minus(feeDiff)
|
||||
|
||||
if (decimalCryptoValue < BigDecimal.ZERO) return state
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
|||
import com.tangem.features.send.impl.presentation.state.*
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.checkIfFeeTooHigh
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isDogecoin
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendNotificationFactory(
|
||||
|
|
@ -57,6 +56,7 @@ internal class SendNotificationFactory(
|
|||
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
|
||||
buildList {
|
||||
// errors
|
||||
addFeeUnreachableNotification(feeState.feeSelectorState)
|
||||
addExceedBalanceNotification(feeAmount, amountValue)
|
||||
addExceedsBalanceNotification(feeState.fee)
|
||||
addMinimumAmountErrorNotification(feeAmount, amountValue)
|
||||
|
|
@ -70,7 +70,7 @@ internal class SendNotificationFactory(
|
|||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun dismissNotificationState(clazz: Class<out SendNotification>): SendUiState {
|
||||
fun dismissNotificationState(clazz: Class<out SendNotification>, isIgnored: Boolean = false): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
val notificationsToRemove = sendState.notifications.filterIsInstance(clazz)
|
||||
|
|
@ -78,50 +78,35 @@ internal class SendNotificationFactory(
|
|||
updatedNotifications.removeAll(notificationsToRemove)
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
ignoreAmountReduce = true,
|
||||
ignoreAmountReduce = isIgnored,
|
||||
notifications = updatedNotifications.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState is FeeSelectorState.Error) {
|
||||
add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addExceedBalanceNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
||||
val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider()
|
||||
val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val coinCryptoAmount = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val showNotification = if (cryptoCurrencyStatus.currency.id == feePaidCryptoCurrencyStatus?.currency?.id) {
|
||||
receivedAmount > cryptoAmount || feeAmount > coinCryptoAmount
|
||||
} else {
|
||||
receivedAmount + feeAmount > cryptoAmount
|
||||
}
|
||||
val isCurrentNotFeePaidCurrency = cryptoCurrencyStatus.currency.id != feePaidCryptoCurrencyStatus?.currency?.id
|
||||
if (isCurrentNotFeePaidCurrency) return
|
||||
|
||||
val showNotification = receivedAmount + feeAmount > cryptoAmount
|
||||
if (showNotification) {
|
||||
add(SendNotification.Error.TotalExceedsBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
||||
|
||||
val totalAmount = feeAmount + receivedAmount
|
||||
val balance = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
if (isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)) {
|
||||
val minimum = BigDecimal(DOGECOIN_MINIMUM)
|
||||
if (receivedAmount < minimum || balance - totalAmount < minimum) {
|
||||
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo temporarily disabling notification
|
||||
// private suspend fun MutableList<SendNotification>.addReserveAmountErrorNotification(recipientAddress: String) {
|
||||
// val userWalletId = userWalletProvider().walletId
|
||||
|
|
@ -198,7 +183,7 @@ internal class SendNotificationFactory(
|
|||
if (currencyDeposit != null && currencyDeposit > diff) {
|
||||
add(
|
||||
SendNotification.Error.ExistentialDeposit(
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = currencyDeposit,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
|
|
@ -233,6 +218,20 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
||||
val minimum = BigDecimal(DOGECOIN_MINIMUM)
|
||||
|
||||
val isDogecoin = isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)
|
||||
val isExceedDustLimit = checkDustLimits(feeAmount, receivedAmount, minimum)
|
||||
if (isDogecoin && isExceedDustLimit) {
|
||||
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
|
|
@ -241,18 +240,12 @@ internal class SendNotificationFactory(
|
|||
val dustValue = currencyChecksRepository.getDustValue(
|
||||
userWalletProvider().walletId,
|
||||
cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
if (dustValue != null && !balance.isNullOrZero() && receivedAmount < balance) {
|
||||
val totalAmount = feeAmount + receivedAmount
|
||||
val change = balance - totalAmount
|
||||
val isChangeLowerThanDust = change < dustValue && change != BigDecimal.ZERO
|
||||
val isShowWarning = totalAmount < dustValue || isChangeLowerThanDust
|
||||
if (isShowWarning) {
|
||||
add(
|
||||
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
|
||||
)
|
||||
}
|
||||
) ?: return
|
||||
|
||||
if (checkDustLimits(feeAmount, receivedAmount, dustValue)) {
|
||||
add(
|
||||
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -274,13 +267,9 @@ internal class SendNotificationFactory(
|
|||
|
||||
private fun MutableList<SendNotification>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState !is FeeSelectorState.Content) return
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
|
||||
val highValue = multipleFees.priority.amount.value ?: return
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = (customValue / highValue).toBigInteger()
|
||||
if (feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
|
||||
add(SendNotification.Warning.TooHigh(diff.toString()))
|
||||
|
||||
checkIfFeeTooHigh(feeSelectorState) { diff ->
|
||||
add(SendNotification.Warning.TooHigh(diff))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -361,8 +350,17 @@ internal class SendNotificationFactory(
|
|||
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
|
||||
}
|
||||
|
||||
private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val totalAmount = feeAmount + receivedAmount
|
||||
val change = balance - totalAmount
|
||||
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
|
||||
return receivedAmount < dustValue || isChangeLowerThanDust
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DOGECOIN_MINIMUM = "0.01"
|
||||
internal val FEE_MAX_DIFF = BigInteger("5")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
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.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Check if sending amount with fee is greater than balance
|
||||
|
|
@ -32,14 +34,16 @@ internal fun checkIfFeeTooLow(state: SendUiState): Boolean {
|
|||
/**
|
||||
* Check if custom fee is too high
|
||||
*/
|
||||
internal fun checkIfFeeTooHigh(state: SendUiState, onShow: (String) -> Unit): Boolean {
|
||||
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onShow: (String) -> Unit): Boolean {
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
|
||||
val highValue = multipleFees.priority.amount.value ?: return false
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = (customValue / highValue).toBigInteger()
|
||||
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > SendNotificationFactory.FEE_MAX_DIFF
|
||||
if (isShow) onShow(diff.toString())
|
||||
val diff = customValue / highValue
|
||||
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF
|
||||
if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP))
|
||||
return isShow
|
||||
}
|
||||
}
|
||||
|
||||
private val FEE_MAX_DIFF = BigDecimal("5")
|
||||
private const val ZERO_DECIMALS = 0
|
||||
|
|
@ -15,6 +15,8 @@ internal sealed class FeeSelectorState {
|
|||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
|
||||
data object Loading : FeeSelectorState()
|
||||
|
||||
data object Error : FeeSelectorState()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ internal class FeeStateFactory(
|
|||
return state.copy(
|
||||
amountState = state.amountState?.copy(isFeeLoading = true),
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = if (feeState.feeSelectorState is FeeSelectorState.Content) {
|
||||
feeState.feeSelectorState
|
||||
} else {
|
||||
FeeSelectorState.Loading
|
||||
},
|
||||
notifications = persistentListOf(),
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal class SendFeeStateConverter(
|
|||
|
||||
override fun convert(value: Unit): SendStates.FeeState {
|
||||
return SendStates.FeeState(
|
||||
feeSelectorState = FeeSelectorState.Error,
|
||||
feeSelectorState = FeeSelectorState.Loading,
|
||||
fee = null,
|
||||
notifications = persistentListOf(),
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ internal object AmountStatePreviewData {
|
|||
isError = false,
|
||||
error = TextReference.EMPTY,
|
||||
),
|
||||
isSegmentedButtonsEnabled = true,
|
||||
)
|
||||
|
||||
val fiatAmountState = amountState.copy(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -81,7 +81,7 @@ internal class SendRecipientHistoryListConverter(
|
|||
}
|
||||
|
||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||
val date = timestampInMillis.toDateFormat(
|
||||
val date = timestampInMillis.toDateFormatWithTodayYesterday(
|
||||
formatter = DateTimeFormatters.dateDDMMYYYY,
|
||||
)
|
||||
val time = timestampInMillis.toTimeFormat()
|
||||
|
|
|
|||
|
|
@ -18,22 +18,25 @@ internal class SendRecipientWalletListConverter :
|
|||
}
|
||||
}
|
||||
|
||||
private fun List<AvailableWallet?>.filterWallets() = this.filterNotNull()
|
||||
.groupBy { item -> item.name }
|
||||
.values.map {
|
||||
it.mapIndexed { index, item ->
|
||||
val name = if (it.size > 1) {
|
||||
"${item.name} ${index.inc()}"
|
||||
} else {
|
||||
item.name
|
||||
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
|
||||
var walletsCounter = 0
|
||||
return this.filterNotNull()
|
||||
.groupBy { item -> item.name }
|
||||
.values.map {
|
||||
it.mapIndexed { index, item ->
|
||||
val name = if (it.size > 1) {
|
||||
"${item.name} ${index.inc()}"
|
||||
} else {
|
||||
item.name
|
||||
}
|
||||
SendRecipientListContent(
|
||||
id = "${WALLET_KEY_TAG}${walletsCounter++}",
|
||||
title = TextReference.Str(item.address),
|
||||
subtitle = TextReference.Str(name),
|
||||
)
|
||||
}
|
||||
SendRecipientListContent(
|
||||
id = "${WALLET_KEY_TAG}$index",
|
||||
title = TextReference.Str(item.address),
|
||||
subtitle = TextReference.Str(name),
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ internal fun LazyListScope.buttons(
|
|||
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
clickIntents: SendClickIntents,
|
||||
isMaxButtonEnabled: Boolean,
|
||||
isSegmentedButtonsEnabled: Boolean,
|
||||
) {
|
||||
item(
|
||||
key = AMOUNT_BUTTONS_KEY,
|
||||
|
|
@ -48,8 +49,12 @@ internal fun LazyListScope.buttons(
|
|||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clickIntents.onCurrencyChangeClick(it.isFiat)
|
||||
},
|
||||
isEnabled = isSegmentedButtonsEnabled,
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
SendAmountCurrencyButton(
|
||||
button = it,
|
||||
isSegmentedButtonsEnabled = isSegmentedButtonsEnabled,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SpacerWMax()
|
||||
|
|
@ -72,7 +77,7 @@ internal fun LazyListScope.buttons(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
||||
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -86,17 +91,16 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
|||
FiatIcon(
|
||||
url = button.iconUrl,
|
||||
size = TangemTheme.dimens.size18,
|
||||
isGrayscale = !isSegmentedButtonsEnabled,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
} else {
|
||||
button.iconState?.let {
|
||||
TokenIcon(
|
||||
state = it,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
}
|
||||
} else if (button.iconState != null) {
|
||||
TokenIcon(
|
||||
state = button.iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = button.title.resolveReference(),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ internal fun SendAmountContent(
|
|||
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
||||
clickIntents = clickIntents,
|
||||
isMaxButtonEnabled = !amountState.isFeeLoading,
|
||||
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
|
||||
)
|
||||
notifications(amountState.notifications)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.send
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
|
|
@ -26,20 +27,12 @@ import com.tangem.features.send.impl.presentation.utils.getFiatReference
|
|||
|
||||
@Composable
|
||||
internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||
val fee = feeState.fee ?: return
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val (title, icon) = when (feeSelectorState.selectedFee) {
|
||||
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_fee_selector_option_custom to R.drawable.ic_edit_24
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess) { onClick() }
|
||||
.clickable(enabled = !isSuccess && feeState.fee != null) { onClick() }
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -47,17 +40,71 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick
|
|||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
SelectorRowItem(
|
||||
titleRes = title,
|
||||
iconRes = icon,
|
||||
preDot = getCryptoReference(fee.amount, feeState.isFeeApproximate),
|
||||
postDot = getFiatReference(fee.amount, feeState.rate, feeState.appCurrency),
|
||||
ellipsizeOffset = fee.amount.currencySymbol.length,
|
||||
isSelected = true,
|
||||
showDivider = false,
|
||||
paddingValues = PaddingValues(),
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
) {
|
||||
val feeSelectorState = feeState.feeSelectorState
|
||||
val feeAmount = feeState.fee?.amount
|
||||
val (title, icon) = if (feeSelectorState is FeeSelectorState.Content) {
|
||||
when (feeSelectorState.selectedFee) {
|
||||
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_fee_selector_option_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 = getCryptoReference(feeAmount, feeState.isFeeApproximate),
|
||||
postDot = feeAmount?.let { getFiatReference(it, feeState.rate, feeState.appCurrency) },
|
||||
ellipsizeOffset = feeAmount?.currencySymbol?.length,
|
||||
isSelected = true,
|
||||
showDivider = false,
|
||||
paddingValues = PaddingValues(),
|
||||
)
|
||||
FeeLoading(feeSelectorState)
|
||||
FeeError(feeSelectorState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.FeeLoading(feeSelectorState: FeeSelectorState) {
|
||||
AnimatedContent(
|
||||
targetState = feeSelectorState,
|
||||
label = "Fee Loading State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeSelectorState.Loading) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
height = TangemTheme.dimens.size12,
|
||||
width = TangemTheme.dimens.size90,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) {
|
||||
AnimatedContent(
|
||||
targetState = feeSelectorState,
|
||||
label = "Fee Error State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeSelectorState.Error) {
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -40,6 +41,7 @@ internal fun RecipientBlock(
|
|||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AddressBlock(recipientState.addressTextField)
|
||||
MemoBlock(recipientState.memoTextField)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +72,28 @@ private fun AddressBlock(address: SendTextField.RecipientAddress) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemoBlock(memo: SendTextField.RecipientMemo?) {
|
||||
val showMemo = memo != null && memo.value.isNotBlank()
|
||||
if (showMemo) {
|
||||
HorizontalDivider(
|
||||
color = TangemTheme.colors.icon.inactive,
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
Text(
|
||||
text = memo?.label?.resolveReference().orEmpty(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = memo?.value.orEmpty(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.domain.tokens.*
|
|||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
|
|
@ -359,13 +360,14 @@ internal class SendViewModel @Inject constructor(
|
|||
stateRouter.showSend()
|
||||
}
|
||||
transactionId != null && amount != null && destinationAddress != null -> {
|
||||
loadFee()
|
||||
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||
stateRouter.showFee()
|
||||
stateRouter.showSend()
|
||||
updateNotifications()
|
||||
}
|
||||
else -> {
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
getWalletsAndRecent()
|
||||
stateRouter.showRecipient()
|
||||
updateNotifications()
|
||||
}
|
||||
|
|
@ -406,7 +408,8 @@ internal class SendViewModel @Inject constructor(
|
|||
return if (!isMultiCurrency) {
|
||||
val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull()
|
||||
val address = status?.value?.networkAddress.takeIf {
|
||||
status?.currency?.network?.id == cryptoCurrency.network.id
|
||||
status?.currency?.network?.id == cryptoCurrency.network.id &&
|
||||
status.currency.network.derivationPath !is Network.DerivationPath.Custom
|
||||
}
|
||||
address?.let {
|
||||
AvailableWallet(
|
||||
|
|
@ -417,7 +420,8 @@ internal class SendViewModel @Inject constructor(
|
|||
} else {
|
||||
val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull()
|
||||
val walletCurrency = statuses?.firstOrNull {
|
||||
it.currency.network.id == cryptoCurrency.network.id
|
||||
it.currency.network.id == cryptoCurrency.network.id &&
|
||||
it.currency.network.derivationPath !is Network.DerivationPath.Custom
|
||||
}
|
||||
val address = walletCurrency?.value?.networkAddress
|
||||
address?.let {
|
||||
|
|
@ -522,8 +526,9 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
return true
|
||||
}
|
||||
val feeSelectorState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
return checkIfFeeTooHigh(
|
||||
state = uiState,
|
||||
feeSelectorState = feeSelectorState,
|
||||
onShow = { diff ->
|
||||
uiState = eventStateFactory.getFeeTooHighAlert(
|
||||
diff = diff,
|
||||
|
|
@ -702,6 +707,7 @@ internal class SendViewModel @Inject constructor(
|
|||
if (result == null) {
|
||||
onFeeLoadFailed(isShowStatus, isToNextState)
|
||||
}
|
||||
updateNotifications()
|
||||
updateFeeNotifications()
|
||||
}.saveIn(feeJobHolder)
|
||||
.invokeOnCompletion {
|
||||
|
|
@ -735,7 +741,7 @@ internal class SendViewModel @Inject constructor(
|
|||
return getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
userWallet = userWallet,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
|
@ -792,10 +798,11 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
|
||||
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
|
||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction() {
|
||||
|
|
|
|||
|
|
@ -126,14 +126,15 @@ class SwapDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideSendTransactionUseCase(
|
||||
@SwapScope isDemoCardUseCase: IsDemoCardUseCase,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
demoConfig = DemoConfig(),
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
transactionRepository = transactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -646,6 +646,7 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning {
|
||||
val providerErrorMessage = getProviderErrorMessage(dataError)
|
||||
return when (dataError) {
|
||||
is DataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
|
||||
notificationConfig = NotificationConfig(
|
||||
|
|
@ -674,10 +675,16 @@ internal class StateBuilder(
|
|||
} else {
|
||||
resourceReference(R.string.warning_express_refresh_required_title)
|
||||
},
|
||||
subtitle = if (dataError is DataError.UnknownError) {
|
||||
resourceReference(R.string.common_unknown_error)
|
||||
} else {
|
||||
resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
||||
subtitle = when {
|
||||
dataError is DataError.UnknownError -> {
|
||||
resourceReference(R.string.common_unknown_error)
|
||||
}
|
||||
providerErrorMessage != null -> {
|
||||
providerErrorMessage
|
||||
}
|
||||
else -> {
|
||||
resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
||||
}
|
||||
},
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
|
|
@ -972,7 +979,7 @@ internal class StateBuilder(
|
|||
return uiState.copy(
|
||||
alert = SwapWarning.GenericWarning(
|
||||
message = if (swapTransactionState is SwapTransactionState.ExpressError) {
|
||||
getAlertErrorMessage(swapTransactionState.dataError)
|
||||
getProviderErrorMessage(swapTransactionState.dataError)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
|
@ -987,7 +994,7 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getAlertErrorMessage(dataError: DataError): TextReference? {
|
||||
private fun getProviderErrorMessage(dataError: DataError): TextReference? {
|
||||
return when (dataError) {
|
||||
is DataError.SwapsAreUnavailableNowError -> resourceReference(
|
||||
id = R.string.express_error_swap_unavailable,
|
||||
|
|
@ -998,6 +1005,7 @@ internal class StateBuilder(
|
|||
formatArgs = wrappedList(dataError.code),
|
||||
)
|
||||
is DataError.ExchangeProviderNotActiveError,
|
||||
is DataError.ExchangeProviderNotFoundError,
|
||||
is DataError.ExchangeProviderNotAvailableError,
|
||||
is DataError.ExchangeProviderProviderInternalError,
|
||||
-> resourceReference(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -71,7 +71,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
txUrl = transaction.status?.txExternalUrl,
|
||||
txExternalId = transaction.status?.txExternalId,
|
||||
timestamp = TextReference.Str(
|
||||
"${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}",
|
||||
"${timestamp.toDateFormatWithTodayYesterday()}, ${timestamp.toTimeFormat()}",
|
||||
),
|
||||
fiatSymbol = appCurrency.symbol,
|
||||
statuses = getStatuses(transaction.status?.status),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.paging.*
|
|||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
|
|
@ -68,7 +68,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
// Use raw timestamp to get date
|
||||
|
||||
// If [afterDate] is the first transaction in the flow, add the group title
|
||||
val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
||||
val afterDate = after.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||
if (before is TxHistoryItemState.Title) {
|
||||
return@insertSeparators TxHistoryItemState.GroupTitle(
|
||||
title = afterDate,
|
||||
|
|
@ -80,7 +80,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
||||
* the new group
|
||||
*/
|
||||
val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
||||
val beforeDate = before.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||
return@insertSeparators if (beforeDate != afterDate) {
|
||||
TxHistoryItemState.GroupTitle(
|
||||
title = afterDate,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.blockchain.common.address.AddressType
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
||||
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -165,7 +164,6 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
viewModel = this,
|
||||
deepLinks = listOf(
|
||||
BuyCurrencyDeepLink(::onBuyCurrencyDeepLink),
|
||||
SellCurrencyDeepLink(::onSellCurrencyDeepLink),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -175,20 +173,6 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol))
|
||||
}
|
||||
|
||||
private fun onSellCurrencyDeepLink(data: SellCurrencyDeepLink.Data) {
|
||||
sendCurrency(
|
||||
status = cryptoCurrencyStatus ?: return,
|
||||
transactionInfo = data.let {
|
||||
TransactionInfo(
|
||||
transactionId = it.transactionId,
|
||||
destinationAddress = it.depositWalletAddress,
|
||||
amount = it.baseCurrencyAmount,
|
||||
tag = it.depositWalletAddressTag,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
analyticsEventsHandler.send(
|
||||
event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol),
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ import com.tangem.core.deeplink.DeepLinksRegistry
|
|||
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
||||
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -29,23 +27,20 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
|
||||
private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
) {
|
||||
|
||||
private var deepLinks: List<DeepLink> = emptyList()
|
||||
private var deepLinksMap = mutableMapOf<UserWalletId, List<DeepLink>>()
|
||||
|
||||
fun registerForSingleCurrencyWallets(viewModel: ViewModel, userWallet: UserWallet) {
|
||||
if (userWallet.isMultiCurrency) {
|
||||
deepLinksRegistry.unregister(deepLinks)
|
||||
} else {
|
||||
if (deepLinks.isEmpty()) {
|
||||
deepLinks = getDeepLinks(userWallet, viewModel.viewModelScope)
|
||||
}
|
||||
|
||||
deepLinksRegistry.register(deepLinks)
|
||||
fun registerForWallet(viewModel: ViewModel, userWallet: UserWallet) {
|
||||
val deepLinks = deepLinksMap.getOrPut(userWallet.walletId) {
|
||||
getDeepLinks(userWallet, viewModel.viewModelScope)
|
||||
}
|
||||
deepLinksRegistry.unregisterByIds(deepLinks.map { it.id })
|
||||
deepLinksRegistry.register(deepLinks)
|
||||
|
||||
viewModel.addCloseable {
|
||||
deepLinksRegistry.unregister(deepLinks)
|
||||
|
|
@ -60,20 +55,23 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
|||
}
|
||||
},
|
||||
)
|
||||
val buyCurrencyDeepLink = BuyCurrencyDeepLink(
|
||||
onReceive = {
|
||||
scope.launch {
|
||||
onBuyCurrencyDeepLink(userWallet)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return listOf(sellCurrencyDeepLink, buyCurrencyDeepLink)
|
||||
return buildList {
|
||||
add(sellCurrencyDeepLink)
|
||||
if (!userWallet.isMultiCurrency) {
|
||||
add(
|
||||
BuyCurrencyDeepLink(
|
||||
onReceive = {
|
||||
scope.launch { onBuyCurrencyDeepLink(userWallet) }
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) {
|
||||
val cryptoCurrencyStatus = getCryptoCurrencyStatusSyncUseCase(userWallet.walletId)
|
||||
.getOrNull() ?: return
|
||||
val cryptoCurrencyStatus = findCryptoCurrencyStatus(userWallet, data.currencyId) ?: return
|
||||
val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWallet.walletId,
|
||||
cryptoCurrencyStatus,
|
||||
|
|
@ -110,6 +108,19 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
|||
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
||||
}
|
||||
|
||||
private suspend fun findCryptoCurrencyStatus(
|
||||
userWallet: UserWallet,
|
||||
currencyIdValue: String,
|
||||
): CryptoCurrencyStatus? {
|
||||
return if (userWallet.isMultiCurrency) {
|
||||
getCryptoCurrencyStatusesSyncUseCase(userWallet.walletId).getOrNull()?.let { currencies ->
|
||||
currencies.find { currencyIdValue == it.currency.id.value }
|
||||
}
|
||||
} else {
|
||||
getCryptoCurrencyStatusSyncUseCase(userWallet.walletId).getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendCoin(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.paging.*
|
|||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -50,7 +50,7 @@ internal class TxHistoryItemFlowConverter(
|
|||
// Use raw timestamp to get date
|
||||
|
||||
// If [afterDate] is the first transaction in the flow, add the group title
|
||||
val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
||||
val afterDate = after.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||
if (before is TxHistoryItemState.Title) {
|
||||
return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ internal class TxHistoryItemFlowConverter(
|
|||
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
||||
* the new group
|
||||
*/
|
||||
val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
||||
val beforeDate = before.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||
return@insertSeparators if (beforeDate != afterDate) {
|
||||
TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
selectedWalletAnalyticsSender.send(selectedWallet)
|
||||
}
|
||||
|
||||
walletDeepLinksHandler.registerForSingleCurrencyWallets(
|
||||
walletDeepLinksHandler.registerForWallet(
|
||||
viewModel = this,
|
||||
userWallet = selectedWallet,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ leakcanary = "2.13"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.9-600"
|
||||
tangemBlockchainSdk = "release-app_5.9-605"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.9-343"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue