Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-30 18:31:14 +03:00
parent 6796e868c6
commit ef389a6735
22 changed files with 867 additions and 36 deletions

View file

@ -89,6 +89,12 @@
<string name="common_exchange">Обменять</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_selector_title">Скорость и комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
<string name="common_fee_selector_option_slow">Медленно</string>
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_generate_addresses">Сгенерировать адреса</string>
<string name="common_import">Импортировать</string>
<string name="common_like">Нравится</string>
@ -100,6 +106,7 @@
<string name="common_no_data">Нет данных</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_paste">Вставить</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
@ -384,19 +391,52 @@
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %1$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
<string name="send_datetime_formatter">%1$s в %2$s</string>
<string name="send_enter_address_field">Введите адрес</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %1$s. Ошибки могут привести к потере ваших токенов.</string>
<string name="send_recipient_memo_footer">Мемо/Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_fee_include_description">Включая комиссию</string>
<string name="send_fee_label">Комиссия</string>
<string name="send_fee_picker_low">Низкая</string>
<string name="send_fee_picker_normal">Нормальная</string>
<string name="send_fee_picker_priority">Приоритетная</string>
<string name="send_gas_price">Цена газа</string>
<string name="send_gas_price_footer">Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана.</string>
<string name="send_gas_limit">Лимит газа</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_amount">Всё</string>
<string name="send_max_fee_footer">Максимальная cумма комиссии</string>
<string name="send_max_fee">Комиссия не превысит</string>
<string name="send_network_fee_title">Сетевая комиссия</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса.</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению.</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма резерва не может быть менее %1$s.</string>
<string name="send_notification_top_up_text">Пожалуйста, пополните свой баланс, чтобы продолжить.</string>
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
<string name="send_notification_exceed_fee_text">Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети.</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_optional_field">Необязательное</string>
<string name="send_qrcode_scan_amount_alert_title">QR код содержит информацию о сумме отправки равной %s</string>
<string name="send_recipient">Получатель</string>
<string name="send_recipient_wallets_title">Мои кошельки</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>

View file

@ -87,6 +87,12 @@
<string name="common_exchange">Exchange</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explorer">Explorer</string>
<string name="common_fee_selector_title">Speed and fee</string>
<string name="common_fee_selector_footer">Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority</string>
<string name="common_fee_selector_option_slow">Slow</string>
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_generate_addresses">Generate addresses</string>
<string name="common_import">Import</string>
<string name="common_learn_and_earn">Learn &amp; Earn</string>
@ -99,6 +105,7 @@
<string name="common_no_data">No data</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_paste">Paste</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
@ -383,20 +390,64 @@
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %1$s</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_destination_tag_field">Destination Tag</string>
<string name="send_datetime_formatter">%1$s at %2$s</string>
<string name="send_enter_address_field">Enter address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_insufficient_funds">Insufficient funds for transfer</string>
<string name="send_fee_include_description">Include fee</string>
<string name="send_fee_label">Fee</string>
<string name="send_fee_picker_low">Low</string>
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priority</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_gas_limit">Gas limit</string>
<string name="send_gas_limit_footer">Gas Limit is auto-calculated; raise it during network congestion</string>
<string name="send_gas_price">Gas price</string>
<string name="send_gas_price_footer">Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed.</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_max_amount">Max</string>
<string name="send_max_fee_footer">Maximum fee amount</string>
<string name="send_max_fee">Max fee</string>
<string name="send_memo_destination_tag_error">Numbers only for Destination Tag</string>
<string name="send_network_fee_title">Network fee</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_network_fee_warning_content">Sending amount will be reduced to cover the selected fee level</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance.</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value.</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.</string>
<string name="send_notification_invalid_reserve_amount_title">The balance amount must be at least %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Please top up your balance to continue.</string>
<string name="send_notification_high_fee_title">Fee is increased</string>
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
<string name="send_notification_exceed_fee_text">The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance.</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_optional_field">Optional</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_qrcode_scan_address_success">Recipients address scanned</string>
<string name="send_qrcode_scan_memo_success">Sending amount was changed</string>
<string name="send_qrcode_scan_amount_alert_title">QR code contains information about the sending amount equal to %s</string>
<string name="send_qrcode_scan_amount_alert_text">Change the entered amount?</string>
<string name="send_qrcode_alert_change">Change</string>
<string name="send_qrcode_alert_decline">Decline</string>
<string name="send_recent_transactions">Recent</string>
<string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_wallets_title">My wallets</string>
<string name="send_recipient_address_footer">Ensure that you are sending funds to an %1$s wallet address. Errors may result in the loss of your tokens.</string>
<string name="send_recipient_memo_footer">A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds.</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -404,7 +455,6 @@
<string name="send_total_subtitle_format">%s will be sent</string>
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
<string name="send_validation_invalid_address">Invalid address</string>
<string name="send_insufficient_funds">Insufficient funds for transfer</string>
<string name="shop_buy_now">Buy now</string>
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
<string name="shop_one_wallet">Tangem Wallet</string>

View file

@ -0,0 +1,115 @@
package com.tangem.core.ui.components.icons.identicon
import kotlin.math.abs
import kotlin.math.floor
/**
* Ident icon. Blockies algorithm.
* @see [Blockies](https://github.com/ethereum/blockies)
*
* @param address Address to generate the ident icon
*/
@Suppress("MagicNumber")
internal data class Blockies(
val address: String,
) {
val primaryColor: Int
val backgroundColor: Int
val spotColor: Int
val data: MutableList<Float>
init {
val seed = seedFromAddress(address)
// IMPORTANT! Color generation is based on the seed, thus ORDER is important.
primaryColor = colorFromSeed(seed)
backgroundColor = colorFromSeed(seed)
spotColor = colorFromSeed(seed)
data = dataFromSeed(seed)
}
private fun seedFromAddress(address: String): MutableList<Long> {
val seed = MutableList(SIZE) { DEFAULT_VALUE_L }
address.indices.forEach { index ->
seed[index % HALF_SIZE] =
(seed[index % HALF_SIZE] shl 5) - seed[index % HALF_SIZE] + Character.codePointAt(address, index)
}
// Important cast to preserve JavaScript type behavior
seed.indices.map { seed[it] = seed[it].toInt().toLong() }
return seed
}
private fun colorFromSeed(seed: MutableList<Long>): Int {
val h = floor(nextSeed(seed) * DEGREE_CIRCLE_F)
val s = nextSeed(seed) * SATURATION_MAX + SATURATION_MIN
val l = (nextSeed(seed) + nextSeed(seed) + nextSeed(seed) + nextSeed(seed)) * PROBABILITY_LIGHTNESS
return toRgb(h, s, l)
}
private fun nextSeed(seed: MutableList<Long>): Float {
val t = (seed[0] xor (seed[0] shl 11)).toInt()
seed[0] = seed[1]
seed[1] = seed[2]
seed[2] = seed[3]
seed[3] = seed[3] xor (seed[3] shr 19) xor t.toLong() xor (t shr 8).toLong()
return abs(seed[3]).toFloat() / Integer.MAX_VALUE
}
private fun dataFromSeed(seed: MutableList<Long>) = MutableList(SIZE * SIZE) { DEFAULT_VALUE_F }.apply {
(0 until SIZE).forEach { row ->
(0 until HALF_SIZE).forEach { column ->
val value = floor(nextSeed(seed) * PROBABILITY_COLOR)
this[row * SIZE + column] = value
this[(row + 1) * SIZE - column - 1] = value
}
}
}
private fun toRgb(paramH: Float, paramS: Float, paramL: Float): Int {
val h = paramH % DEGREE_CIRCLE_F / DEGREE_CIRCLE_F
val s = paramS / PERCENT_MAX
val l = paramL / PERCENT_MAX
val q = if (l < 0.5) l * (1 + s) else l + s - s * l
val p = 2 * l - q
val r = hueToRGB(p, q, h + 1f / 3f).coerceIn(0f, 1f)
val g = hueToRGB(p, q, h).coerceIn(0f, 1f)
val b = hueToRGB(p, q, h - 1f / 3f).coerceIn(0f, 1f)
val red = (r * HEX_COLOR_MAX).toInt()
val green = (g * HEX_COLOR_MAX).toInt()
val blue = (b * HEX_COLOR_MAX).toInt()
return RGB_ALPHA_PART shl RGB_R_PART or (red shl RGB_G_PART) or (green shl RGB_B_PART) or blue
}
private fun hueToRGB(p: Float, q: Float, h: Float): Float {
var hue = h
if (hue < 0) hue += 1f
if (hue > 1) hue -= 1f
if (6 * hue < 1) return p + (q - p) * 6f * hue
if (2 * hue < 1) return q
return if (3 * hue < 2) p + (q - p) * 6f * (2f / 3f - hue) else p
}
companion object {
internal const val SIZE = 8
private const val HALF_SIZE = SIZE / 2
private const val DEFAULT_VALUE_L = 0L
private const val DEFAULT_VALUE_F = 0f
private const val DEGREE_CIRCLE_F = 360.0f
private const val HEX_COLOR_MAX = 255
private const val PERCENT_MAX = 100f
private const val SATURATION_MIN = 40f
private const val SATURATION_MAX = 60f
private const val PROBABILITY_LIGHTNESS = 25f
private const val PROBABILITY_COLOR = 2.3f
private const val RGB_ALPHA_PART = 0xFF
private const val RGB_R_PART = 24
private const val RGB_G_PART = 16
private const val RGB_B_PART = 8
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.core.ui.components.icons.identicon
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemTheme
private const val GAP_WIDTH = 1f
private const val ALPHA = 0.9f
/**
* Ident icon
*
* @param address to generate ident icon from
* @param modifier to apply to ident icon
*/
@Suppress("MagicNumber")
@Composable
fun IdentIcon(address: String, modifier: Modifier = Modifier) {
if (address.isBlank()) {
Box(modifier = modifier)
} else {
val blockies = Blockies(address.toLowerCase(Locale.current))
Canvas(
modifier = modifier
.then(
if (LocalIsInDarkTheme.current) {
Modifier.alpha(ALPHA)
} else {
Modifier
},
),
) {
val cellWidth = size.width / Blockies.SIZE
blockies.data.forEachIndexed { index, item ->
val y = index / Blockies.SIZE
val x = index % Blockies.SIZE
val colorInt = when (item) {
1f -> blockies.primaryColor
2f -> blockies.spotColor
else -> blockies.backgroundColor
}
drawRect(
color = Color(colorInt),
topLeft = Offset(x * cellWidth, y * cellWidth),
size = Size(cellWidth + GAP_WIDTH, cellWidth + GAP_WIDTH),
)
}
}
}
}
//region preview
@Preview
@Composable
private fun IdentIconPreview_Light() {
TangemTheme {
IdentIcon(
address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359",
modifier = Modifier
.size(TangemTheme.dimens.size40),
)
}
}
@Preview
@Composable
private fun IdentIconPreview_Dark() {
TangemTheme(isDark = true) {
IdentIcon(
address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359",
modifier = Modifier
.size(TangemTheme.dimens.size40),
)
}
}
//endregion

View file

@ -31,6 +31,7 @@ data class TangemDimens internal constructor(
val radius14: Dp = 14.dp,
val radius16: Dp = 16.dp,
val radius18: Dp = 18.dp,
val radius20: Dp = 20.dp,
val radius24: Dp = 24.dp,
val radius26: Dp = 26.dp,
val radius28: Dp = 28.dp,

View file

@ -9,9 +9,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.send.ui.SendScreen
import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.ui.SendScreen
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.state
package com.tangem.features.send.impl.presentation.state
import arrow.core.Either
import com.tangem.common.Provider
@ -7,9 +7,10 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
internal class SendStateFactory(
private val clickIntents: SendClickIntents,

View file

@ -1,11 +1,12 @@
package com.tangem.features.send.impl.presentation.send.state
package com.tangem.features.send.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.PersistentList
/**

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.state
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.state
package com.tangem.features.send.impl.presentation.state.amount
import arrow.core.Either
import com.tangem.common.Provider
@ -10,8 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf

View file

@ -1,7 +1,7 @@
package com.tangem.features.send.impl.presentation.send.state.fields
package com.tangem.features.send.impl.presentation.state.fields
import com.tangem.common.Provider
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.converter.Converter
import java.text.DecimalFormatSymbols
import java.text.NumberFormat

View file

@ -1,11 +1,11 @@
package com.tangem.features.send.impl.presentation.send.state.fields
package com.tangem.features.send.impl.presentation.state.fields
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.converter.Converter
import java.text.NumberFormat

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.state.fields
package com.tangem.features.send.impl.presentation.state.fields
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Immutable

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.ui
package com.tangem.features.send.impl.presentation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -18,9 +18,9 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.send.state.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.ui.amount.AmountFieldContainer
@Composable
internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.ui
package com.tangem.features.send.impl.presentation.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
@ -18,7 +18,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiState
@Composable
internal fun SendNavigationButtons(uiState: SendUiState.Content) {

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.ui
package com.tangem.features.send.impl.presentation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
@ -13,7 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiState
@Composable
internal fun SendScreen(uiState: SendUiState.Content) {

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.ui.amount
package com.tangem.features.send.impl.presentation.ui.amount
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@ -27,7 +27,7 @@ import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@Composable
internal fun ColumnScope.AmountField(

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.ui.amount
package com.tangem.features.send.impl.presentation.ui.amount
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
@ -13,7 +13,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiState
@Composable
internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) {

View file

@ -0,0 +1,154 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.MiddleEllipsisText
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
/**
* Row item with title and subtitle
*
* @param title title
* @param subtitle subtitle
* @param onClick click listener
* @param modifier modifier
* @param subtitleIconRes icon
*/
@Composable
fun ListItemWithIcon(
title: String,
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@DrawableRes subtitleIconRes: Int? = null,
) {
Row(
modifier = modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.action)
.clickable { onClick() }
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
),
) {
IdentIcon(
address = title,
modifier = Modifier
.size(TangemTheme.dimens.size40)
.clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing2,
bottom = TangemTheme.dimens.spacing2,
),
) {
MiddleEllipsisText(
text = title,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Justify,
modifier = Modifier.fillMaxSize(),
)
Row {
subtitleIconRes?.let { iconRes ->
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size16)
.background(TangemTheme.colors.background.tertiary, CircleShape)
.padding(TangemTheme.dimens.spacing3),
)
}
Text(
text = subtitle,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
modifier = Modifier
.then(
if (subtitleIconRes != null) {
Modifier.padding(start = TangemTheme.dimens.spacing4)
} else {
Modifier
},
),
)
}
}
}
}
// region preview
@Preview
@Composable
private fun ListItemWithIconPreview_Light(
@PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig,
) {
TangemTheme {
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
subtitleIconRes = config.iconRes,
onClick = {},
)
}
}
@Preview
@Composable
private fun ListItemWithIconPreview_Dark(
@PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig,
) {
TangemTheme(isDark = true) {
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
subtitleIconRes = config.iconRes,
onClick = {},
)
}
}
private data class ListItemWithIconPreviewConfig(
val title: String,
val subtitle: String,
val iconRes: Int? = null,
)
private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider<ListItemWithIconPreviewConfig>(
collection = listOf(
ListItemWithIconPreviewConfig(
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
subtitle = "Wallet",
iconRes = R.drawable.ic_arrow_down_24,
),
ListItemWithIconPreviewConfig(
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
subtitle = "Wallet",
),
),
)
//endregion

View file

@ -0,0 +1,380 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
@Composable
internal fun TextFieldWithPasteAndIcon(
value: String,
placeholder: TextReference,
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
singleLine: Boolean = false,
) {
FooterContainer(modifier, footer) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
Text(
text = label.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
)
Row {
IdentIcon(
address = value,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing10,
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius20))
.size(TangemTheme.dimens.size40)
.background(TangemTheme.colors.background.tertiary),
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
singleLine = singleLine,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing10,
)
.weight(1f)
.align(CenterVertically),
)
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(
start = TangemTheme.dimens.spacing4,
end = TangemTheme.dimens.spacing16,
),
)
}
}
}
}
@Composable
internal fun TextFieldWithPaste(
value: String,
placeholder: TextReference,
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
) {
FooterContainer(modifier, footer) {
Row(
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
Column(
modifier = Modifier
.weight(1f)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = label.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6),
)
}
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(end = TangemTheme.dimens.spacing16),
)
}
}
}
@Composable
internal fun TextFieldWithInfo(
value: String,
label: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
info: TextReference? = null,
footer: String? = null,
) {
FooterContainer(
footer = footer,
footerTopPadding = TangemTheme.dimens.spacing6,
modifier = modifier,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing14,
),
) {
Text(
text = label,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Row {
SimpleTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6)
.weight(1f),
)
info?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
)
}
}
}
}
}
@Composable
private fun FooterContainer(
modifier: Modifier = Modifier,
footer: String? = null,
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
content: @Composable () -> Unit,
) {
Column(modifier = modifier) {
content()
footer?.let {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = footerTopPadding),
)
}
}
}
@Composable
private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
if (isPasteButtonVisible) {
Box(modifier = modifier) {
Text(
text = "Paste",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
color = TangemTheme.colors.button.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = TangemTheme.dimens.spacing10,
vertical = TangemTheme.dimens.spacing2,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
clipboardManager
.getText()
?.toString()
.orEmpty(),
)
},
),
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = { onClick("") },
),
)
}
}
@Composable
private fun SimpleTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TangemTheme.typography.body2,
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
decorationBox = { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.disabled,
modifier = Modifier,
)
}
textValue()
}
},
modifier = modifier
.focusRequester(focusRequester),
)
}
//region preview
@Preview
@Composable
private fun TextFieldPreview_Light() {
TangemTheme {
Column {
TextFieldWithPaste(
value = "",
label = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_enter_address_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithPasteAndIcon(
value = "",
label = TextReference.Res(R.string.send_extras_hint_memo),
placeholder = TextReference.Res(R.string.send_optional_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithInfo(
value = "Text",
label = stringResource(R.string.send_extras_hint_memo),
info = TextReference.Res(R.string.send_optional_field),
footer = stringResource(R.string.send_max_fee),
onValueChange = {},
)
}
}
}
@Preview
@Composable
private fun TextFieldPreview_Dark() {
TangemTheme(isDark = true) {
Column {
TextFieldWithPaste(
value = "",
label = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_enter_address_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithPasteAndIcon(
value = "",
label = TextReference.Res(R.string.send_extras_hint_memo),
placeholder = TextReference.Res(R.string.send_optional_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithInfo(
value = "Text",
label = stringResource(R.string.send_extras_hint_memo),
info = TextReference.Res(R.string.send_optional_field),
footer = stringResource(R.string.send_max_fee),
onValueChange = {},
)
}
}
}
//endregion

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.viewmodel
package com.tangem.features.send.impl.presentation.viewmodel
interface SendClickIntents {

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.send.viewmodel
package com.tangem.features.send.impl.presentation.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -14,8 +14,8 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.presentation.send.state.SendStateFactory
import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendStateFactory
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn