Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-24 16:33:38 +03:00
parent 33406d0ac1
commit 2a9a80414d
39 changed files with 1074 additions and 499 deletions

View file

@ -1,6 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -15,10 +18,25 @@ internal object TransactionDomainModule {
@Provides
@ViewModelScoped
fun provideGetUseCase(
fun provideGetFeeUseCase(
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): GetFeeUseCase {
return GetFeeUseCase(walletManagersFacade, dispatchers)
}
@Provides
@ViewModelScoped
fun provideSendTransactionUseCase(
isDemoCardUseCase: IsDemoCardUseCase,
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
): SendTransactionUseCase {
return SendTransactionUseCase(
isDemoCardUseCase = isDemoCardUseCase,
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
)
}
}

View file

@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -39,7 +42,10 @@ fun AppBarWithBackButtonAndIcon(
contentDescription = null,
modifier = Modifier
.size(size = TangemTheme.dimens.size24)
.clickable { onBackClick() },
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onBackClick() },
tint = TangemTheme.colors.icon.primary1,
)
AnimatedContent(

View file

@ -1,4 +1,4 @@
package com.tangem.core.ui.components.fields
package com.tangem.core.ui.components.fields.visualtransformations
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString

View file

@ -0,0 +1,87 @@
package com.tangem.core.ui.components.transactions
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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.toTimeFormat
/**
* Common transaction done screen title
*
* @param titleRes title resource
* @param date transaction timestamp in millis
*/
@Composable
fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_empty_in_process_64),
contentDescription = null,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size64),
)
Text(
text = stringResource(id = titleRes),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32),
)
Text(
text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4),
)
}
}
// region Previews
@Preview
@Composable
private fun TransactionDoneTitlePreview_Light() {
TangemTheme {
TransactionDoneTitle(
titleRes = R.string.sent_transaction_sent_title,
date = 0,
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing16),
)
}
}
@Preview
@Composable
private fun TransactionDoneTitlePreview_Dark() {
TangemTheme(isDark = true) {
TransactionDoneTitle(
titleRes = R.string.sent_transaction_sent_title,
date = 0,
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing16),
)
}
}
// endregion

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.utils
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
@ -24,6 +25,10 @@ object BigDecimalFormatter {
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
}
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String {
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals)
}
fun formatFiatAmount(
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M28.384,0.616C27.896,0.128 27.104,0.128 26.616,0.616C26.128,1.104 26.128,1.896 26.616,2.384L31.002,6.769C17.519,7.294 6.75,18.389 6.75,32C6.75,45.945 18.055,57.25 32,57.25C45.945,57.25 57.25,45.945 57.25,32C57.25,31.31 56.69,30.75 56,30.75C55.31,30.75 54.75,31.31 54.75,32C54.75,44.564 44.564,54.75 32,54.75C19.435,54.75 9.25,44.564 9.25,32C9.25,19.785 18.878,9.818 30.959,9.273L26.616,13.616C26.128,14.104 26.128,14.896 26.616,15.384C27.104,15.872 27.896,15.872 28.384,15.384L34.884,8.884C35.372,8.396 35.372,7.604 34.884,7.116L28.384,0.616ZM39,22.75H40.25V24V26V26.518L39.884,26.884L33.768,33L39.884,39.116L40.25,39.482V40V42V43.25H39H25H23.75V42V40V39.482L24.116,39.116L30.232,33L24.116,26.884L23.75,26.518V26V24V22.75H25H39ZM26.25,25.482L32,31.232L37.75,25.482V25.25H26.25V25.482ZM37.75,40.518L32,34.768L26.25,40.518V40.75H37.75V40.518Z"
android:fillColor="#C9C9C9"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M12,22C10.633,22 9.342,21.737 8.125,21.212C6.908,20.688 5.846,19.971 4.938,19.063C4.029,18.154 3.313,17.092 2.787,15.875C2.263,14.658 2,13.367 2,12C2,10.617 2.263,9.321 2.787,8.113C3.313,6.904 4.029,5.846 4.938,4.938C5.846,4.029 6.908,3.313 8.125,2.787C9.342,2.263 10.633,2 12,2C13.383,2 14.679,2.263 15.887,2.787C17.096,3.313 18.154,4.029 19.063,4.938C19.971,5.846 20.688,6.904 21.212,8.113C21.737,9.321 22,10.617 22,12C22,13.367 21.737,14.658 21.212,15.875C20.688,17.092 19.971,18.154 19.063,19.063C18.154,19.971 17.096,20.688 15.887,21.212C14.679,21.737 13.383,22 12,22ZM12,19.95C12.433,19.35 12.808,18.725 13.125,18.075C13.442,17.425 13.7,16.733 13.9,16H10.1C10.3,16.733 10.558,17.425 10.875,18.075C11.192,18.725 11.567,19.35 12,19.95ZM9.4,19.55C9.1,19 8.837,18.429 8.613,17.837C8.387,17.246 8.2,16.633 8.05,16H5.1C5.583,16.833 6.188,17.558 6.912,18.175C7.637,18.792 8.467,19.25 9.4,19.55ZM14.6,19.55C15.533,19.25 16.362,18.792 17.087,18.175C17.813,17.558 18.417,16.833 18.9,16H15.95C15.8,16.633 15.613,17.246 15.387,17.837C15.163,18.429 14.9,19 14.6,19.55ZM4.25,14H7.65C7.6,13.667 7.563,13.337 7.537,13.012C7.512,12.688 7.5,12.35 7.5,12C7.5,11.65 7.512,11.313 7.537,10.988C7.563,10.663 7.6,10.333 7.65,10H4.25C4.167,10.333 4.104,10.663 4.063,10.988C4.021,11.313 4,11.65 4,12C4,12.35 4.021,12.688 4.063,13.012C4.104,13.337 4.167,13.667 4.25,14ZM9.65,14H14.35C14.4,13.667 14.438,13.337 14.462,13.012C14.488,12.688 14.5,12.35 14.5,12C14.5,11.65 14.488,11.313 14.462,10.988C14.438,10.663 14.4,10.333 14.35,10H9.65C9.6,10.333 9.563,10.663 9.538,10.988C9.512,11.313 9.5,11.65 9.5,12C9.5,12.35 9.512,12.688 9.538,13.012C9.563,13.337 9.6,13.667 9.65,14ZM16.35,14H19.75C19.833,13.667 19.896,13.337 19.938,13.012C19.979,12.688 20,12.35 20,12C20,11.65 19.979,11.313 19.938,10.988C19.896,10.663 19.833,10.333 19.75,10H16.35C16.4,10.333 16.438,10.663 16.462,10.988C16.487,11.313 16.5,11.65 16.5,12C16.5,12.35 16.487,12.688 16.462,13.012C16.438,13.337 16.4,13.667 16.35,14ZM15.95,8H18.9C18.417,7.167 17.813,6.442 17.087,5.825C16.362,5.208 15.533,4.75 14.6,4.45C14.9,5 15.163,5.571 15.387,6.162C15.613,6.754 15.8,7.367 15.95,8ZM10.1,8H13.9C13.7,7.267 13.442,6.575 13.125,5.925C12.808,5.275 12.433,4.65 12,4.05C11.567,4.65 11.192,5.275 10.875,5.925C10.558,6.575 10.3,7.267 10.1,8ZM5.1,8H8.05C8.2,7.367 8.387,6.754 8.613,6.162C8.837,5.571 9.1,5 9.4,4.45C8.467,4.75 7.637,5.208 6.912,5.825C6.188,6.442 5.583,7.167 5.1,8Z"
android:fillColor="#1E1E1E"/>
</group>
</vector>

View file

@ -13,6 +13,10 @@ android {
dependencies {
implementation(deps.androidx.datastore)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -1,6 +1,7 @@
package com.tangem.data.card
import com.tangem.TangemSdk
import com.tangem.blockchain.common.CommonSigner
import com.tangem.common.UserCodeType
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.UserCodeRequestPolicy
@ -57,4 +58,16 @@ internal class DefaultCardSdkConfigRepository(
}
override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes
override fun getCommonSigner(cardId: String?) = CommonSigner(
tangemSdk = sdk,
cardId = cardId,
initialMessage = null,
)
override fun isLinkedTerminal() = sdk.config.linkedTerminal
override fun setLinkedTerminal(isLinked: Boolean?) {
sdk.config.linkedTerminal = isLinked
}
}

View file

@ -19,4 +19,8 @@ dependencies {
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.card.repository
import com.tangem.TangemSdk
import com.tangem.blockchain.common.CommonSigner
import com.tangem.domain.models.scan.ProductType
/**
@ -28,4 +29,13 @@ interface CardSdkConfigRepository {
/** Check if access code saving is enabled */
fun isAccessCodeSavingEnabled(): Boolean
/** Get common signer by [cardId] */
fun getCommonSigner(cardId: String?): CommonSigner
/** Check if linked terminal is enabled */
fun isLinkedTerminal(): Boolean?
/** Set linked terminal by [isLinked] */
fun setLinkedTerminal(isLinked: Boolean?)
}

View file

@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
import com.tangem.blockchain.extensions.Result
@ -35,6 +36,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
import java.math.BigDecimal
import java.util.EnumSet
@Suppress("LargeClass")
// FIXME: Move to its own module and make internal
@ -431,6 +433,58 @@ class DefaultWalletManagersFacade(
)
}
override suspend fun validateTransaction(
amount: Amount,
fee: Amount?,
userWalletId: UserWalletId,
network: Network,
): EnumSet<TransactionError>? {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
return walletManager?.validateTransaction(amount, fee)
}
override suspend fun createTransaction(
amount: Amount,
fee: Fee,
memo: String?,
destination: String,
userWalletId: UserWalletId,
network: Network,
): TransactionData? {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
val txData = walletManager?.createTransaction(amount, fee, destination)?.copy(
extras = null, // todo add memo [[REDACTED_JIRA]]
)
return txData
}
override suspend fun sendTransaction(
txData: TransactionData,
signer: CommonSigner,
userWalletId: UserWalletId,
network: Network,
): SimpleResult {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
return (walletManager as TransactionSender).send(txData, signer)
}
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
if (tokens.isEmpty()) return

View file

@ -2,13 +2,13 @@ package com.tangem.domain.walletmanager
import arrow.core.Either
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -19,6 +19,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import java.math.BigDecimal
import java.util.EnumSet
// TODO: Move to its own module
/**
@ -161,4 +162,54 @@ interface WalletManagersFacade {
userWalletId: UserWalletId,
network: Network,
): Result<TransactionFee>?
/**
* Validates transaction
*
* @param amount of transaction
* @param fee of transaction
* @param userWalletId selected wallet id
* @param network network of currency
*/
suspend fun validateTransaction(
amount: Amount,
fee: Amount?,
userWalletId: UserWalletId,
network: Network,
): EnumSet<TransactionError>?
/**
* Creates transaction [TransactionData]
*
* @param amount of transaction
* @param fee of transaction
* @param memo of transaction optional
* @param destination address
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Suppress("LongParameterList")
suspend fun createTransaction(
amount: Amount,
fee: Fee,
memo: String?,
destination: String,
userWalletId: UserWalletId,
network: Network,
): TransactionData?
/**
* Sends transaction
*
* @param txData transaction data
* @param signer card signer
* @param userWalletId selected wallet id
* @param network network of currency
*/
suspend fun sendTransaction(
txData: TransactionData,
signer: CommonSigner,
userWalletId: UserWalletId,
network: Network,
): SimpleResult
}

View file

@ -12,4 +12,7 @@ android {
dependencies {
implementation(projects.domain.txhistory.models)
implementation(projects.core.analytics.models)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.tokens.utils
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal
/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */
fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount(
currencySymbol = cryptoCurrency.symbol,
value = this,
decimals = cryptoCurrency.decimals,
type = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.Coin
is CryptoCurrency.Token -> AmountType.Token(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
)
},
)

View file

@ -14,10 +14,15 @@ dependencies {
implementation(projects.core.utils)
/** Tangem SDKs */
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain)
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.demo)
implementation(projects.domain.card)
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.transaction.error
sealed class SendTransactionError {
object DemoCardError : SendTransactionError()
data class DataError(val message: String?) : SendTransactionError()
data class NetworkError(val message: String?) : SendTransactionError()
}

View file

@ -0,0 +1,61 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
class SendTransactionUseCase(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
txData: TransactionData,
userWallet: UserWallet,
network: Network,
): Either<SendTransactionError?, Boolean> {
val signer = cardSdkConfigRepository.getCommonSigner(
userWallet.cardId,
)
val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal()
if (userWallet.scanResponse.card.isStart2Coin) {
cardSdkConfigRepository.setLinkedTerminal(false)
}
val sendResult = try {
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
SendTransactionError.DemoCardError.left()
} else {
walletManagersFacade.sendTransaction(
txData = txData,
signer = signer,
userWalletId = userWallet.walletId,
network = network,
).right()
}
} catch (ex: Exception) {
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
SendTransactionError.DataError(ex.message).left()
}
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
return sendResult.fold(
ifRight = { result ->
when (result) {
is SimpleResult.Success -> true.right()
is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left()
}
},
ifLeft = { it.left() },
)
}
}

View file

@ -45,6 +45,7 @@ dependencies {
implementation(projects.core.featuretoggles)
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
/** Domain modules */
implementation(projects.domain.models)
@ -58,6 +59,8 @@ dependencies {
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.transaction)
implementation(projects.domain.card)
implementation(projects.domain.demo)
/** Feature modules */
implementation(projects.features.send.api)

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.DefaultSendRouter
import dagger.Module
@ -17,7 +18,7 @@ internal object SendRouterModule {
@Provides
@ActivityScoped
fun provideSendRouter(): SendRouter {
return DefaultSendRouter()
fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter {
return DefaultSendRouter(reduxNavController)
}
}

View file

@ -1,9 +1,17 @@
package com.tangem.features.send.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.features.send.impl.presentation.SendFragment
internal class DefaultSendRouter : SendRouter {
internal class DefaultSendRouter(
private val reduxNavController: ReduxNavController,
) : InnerSendRouter {
override fun getEntryFragment(): Fragment = SendFragment.create()
override fun openUrl(url: String) {
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.send.impl.navigation
import com.tangem.features.send.api.navigation.SendRouter
interface InnerSendRouter : SendRouter {
/** Open website by [url] */
fun openUrl(url: String)
}

View file

@ -8,6 +8,8 @@ import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.ui.SendScreen
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
@ -24,12 +26,20 @@ internal class SendFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
lateinit var router: SendRouter
private val viewModel by viewModels<SendViewModel>()
private val innerSendRouter: InnerSendRouter
get() = requireNotNull(router as? InnerSendRouter) {
"innerSendRouter should be instance of InnerSendRouter"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
viewModel.setRouter(
innerSendRouter,
StateRouter(
fragmentManager = WeakReference(parentFragmentManager),
),

View file

@ -0,0 +1,10 @@
package com.tangem.features.send.impl.presentation.domain
sealed class SendNotification {
sealed class Info(val message: String) : SendNotification()
sealed class Critical(val message: String) : SendNotification()
sealed class Error(val message: String) : SendNotification()
}

View file

@ -86,6 +86,7 @@ internal class SendStateFactory(
amountState = amountStateConverter.convert(Unit),
recipientState = recipientStateConverter.convert(Unit),
feeState = feeStateConverter.convert(Unit),
sendState = SendStates.SendState(),
)
//endregion
@ -122,15 +123,15 @@ internal class SendStateFactory(
val recipientState = state.recipientState ?: return state
val isValidMemo = validateMemo(
memo = value,
memo = recipientState.addressTextField.value.value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
val isAddressInWallet = isNotAddressInWallet(
address = value,
walletAddresses = walletAddressesProvider(),
address = recipientState.addressTextField.value.value,
)
val isValidAddress = verifyAddress(
address = recipientState.addressTextField.value.value,
address = value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
@ -138,8 +139,7 @@ internal class SendStateFactory(
it.copy(
value = value,
error = when {
!isValidAddress -> TextReference.Res(R.string.send_recipient_address_error)
!isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
!isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
else -> null
},
isError = !isValidAddress || !isAddressInWallet,
@ -169,11 +169,9 @@ internal class SendStateFactory(
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
// todo add memo validation error text
recipientState.memoTextField?.update {
it.copy(
value = value,
error = TextReference.Res(R.string.send_memo_destination_tag_error),
isError = !isValidMemo,
)
}

View file

@ -23,6 +23,7 @@ internal data class SendUiState(
val amountState: SendStates.AmountState? = null,
val recipientState: SendStates.RecipientState? = null,
val feeState: SendStates.FeeState? = null,
val sendState: SendStates.SendState? = null,
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val currentState: MutableStateFlow<SendUiStateType>,
)
@ -65,11 +66,14 @@ internal sealed class SendStates {
val receivedAmount: MutableStateFlow<String> = MutableStateFlow(""),
) : SendStates()
// todo [REDACTED_JIRA]
/** Send state */
data class SendState(
val isSuccess: Boolean,
)
override val type: SendUiStateType = SendUiStateType.Send,
val isSending: MutableStateFlow<Boolean> = MutableStateFlow(false),
val isSuccess: MutableStateFlow<Boolean> = MutableStateFlow(false),
val transactionDate: MutableStateFlow<Long> = MutableStateFlow(0L),
val txUrl: MutableStateFlow<String> = MutableStateFlow(""),
) : SendStates()
}
enum class SendUiStateType {
@ -77,5 +81,4 @@ enum class SendUiStateType {
Recipient,
Fee,
Send,
Done,
}

View file

@ -9,28 +9,61 @@ internal class StateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
) {
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Amount)
private set
private var isFromSend: Boolean = false
fun popBackStack() {
fragmentManager.get()?.popBackStack()
}
fun onBackClick() {
fragmentManager.get()?.popBackStack()
if (isFromSend) {
showSend()
} else {
when (currentState.value) {
SendUiStateType.Amount -> popBackStack()
SendUiStateType.Recipient -> showAmount()
SendUiStateType.Fee -> showRecipient()
SendUiStateType.Send -> showFee()
}
}
}
fun onNextClick() {
when (currentState.value) {
SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient }
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee }
SendUiStateType.Fee -> currentState.update { SendUiStateType.Send }
SendUiStateType.Send -> currentState.update { SendUiStateType.Done }
SendUiStateType.Done -> onBackClick()
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Recipient -> showFee()
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> onBackClick()
}
}
fun onPrevClick() {
when (currentState.value) {
SendUiStateType.Amount -> onBackClick()
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount }
SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient }
SendUiStateType.Send -> currentState.update { SendUiStateType.Fee }
SendUiStateType.Done -> onBackClick()
SendUiStateType.Amount -> popBackStack()
SendUiStateType.Recipient -> showAmount()
SendUiStateType.Fee -> showRecipient()
SendUiStateType.Send -> popBackStack()
}
}
fun showAmount(isFromSend: Boolean = false) {
this.isFromSend = isFromSend
currentState.update { SendUiStateType.Amount }
}
fun showRecipient(isFromSend: Boolean = false) {
this.isFromSend = isFromSend
currentState.update { SendUiStateType.Recipient }
}
fun showFee(isFromSend: Boolean = false) {
this.isFromSend = isFromSend
currentState.update { SendUiStateType.Fee }
}
fun showSend() {
currentState.update { SendUiStateType.Send }
}
}

View file

@ -45,6 +45,7 @@ internal class SendRecipientMemoFieldConverter(
),
placeholder = TextReference.Res(R.string.send_optional_field),
label = TextReference.Res(value),
error = TextReference.Res(R.string.send_memo_destination_tag_error),
),
)
}

View file

@ -1,23 +1,30 @@
package com.tangem.features.send.impl.presentation.ui
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.components.*
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
@ -64,16 +71,16 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) {
@Composable
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
val currentState = uiState.currentState.collectAsState()
val currentState = uiState.currentState.collectAsStateWithLifecycle()
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false
val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false
val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty()
val buttonTextId = when (currentState.value) {
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> R.string.common_next
SendUiStateType.Send -> R.string.common_send
else -> R.string.common_close
}
val (buttonTextId, buttonClick) = getButtonData(
currentState = currentState,
isSuccess = isSuccess,
uiState = uiState,
)
val isButtonEnabled = when (currentState.value) {
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
@ -86,19 +93,92 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
label = "Update send screen state",
modifier = modifier,
) { textId ->
if (currentState.value == SendUiStateType.Send) {
PrimaryButtonIconEnd(
text = stringResource(textId),
iconResId = R.drawable.ic_tangem_24,
enabled = isButtonEnabled,
onClick = uiState.clickIntents::onNextClick,
)
} else {
PrimaryButton(
text = stringResource(textId),
enabled = isButtonEnabled,
onClick = uiState.clickIntents::onNextClick,
)
when {
currentState.value == SendUiStateType.Send && !isSuccess -> {
PrimaryButtonIconEnd(
text = stringResource(textId),
iconResId = R.drawable.ic_tangem_24,
enabled = isButtonEnabled,
onClick = buttonClick,
showProgress = isSending,
)
}
currentState.value == SendUiStateType.Send && isSuccess -> {
PrimaryButtonsDone(
textRes = textId,
txUrl = txUrl,
onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) },
onDoneClick = buttonClick,
modifier = Modifier,
)
}
else -> {
PrimaryButton(
text = stringResource(textId),
enabled = isButtonEnabled,
onClick = buttonClick,
)
}
}
}
}
@Composable
private fun PrimaryButtonsDone(
@StringRes textRes: Int,
txUrl: String,
onExploreClick: () -> Unit,
onDoneClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
Column(modifier = modifier) {
if (txUrl.isNotBlank()) {
Row {
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_explore),
iconResId = R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
SpacerW12()
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(txUrl)
},
modifier = Modifier.weight(1f),
)
}
SpacerH12()
}
PrimaryButton(
text = stringResource(id = textRes),
enabled = true,
onClick = onDoneClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
private fun getButtonData(
uiState: SendUiState,
currentState: State<SendUiStateType>,
isSuccess: Boolean,
): Pair<Int, () -> Unit> {
return when (currentState.value) {
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> R.string.common_next to uiState.clickIntents::onNextClick
SendUiStateType.Send -> if (isSuccess) {
R.string.common_close
} else {
R.string.common_send
} to uiState.clickIntents::onSendClick
}
}

View file

@ -22,11 +22,13 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent
import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
import com.tangem.features.send.impl.presentation.ui.send.SendContent
@Composable
internal fun SendScreen(uiState: SendUiState) {
val currentState = uiState.currentState.collectAsStateWithLifecycle()
BackHandler { uiState.clickIntents.onPrevClick() }
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()
BackHandler { uiState.clickIntents.onBackClick() }
Column(
modifier = Modifier
.fillMaxSize()
@ -36,12 +38,10 @@ internal fun SendScreen(uiState: SendUiState) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
val titleRes = when (currentState.value) {
SendUiStateType.Amount,
SendUiStateType.Send,
-> R.string.common_send
SendUiStateType.Amount -> R.string.common_send
SendUiStateType.Recipient -> R.string.send_recipient
SendUiStateType.Fee -> R.string.common_fee_selector_title
SendUiStateType.Done -> null
SendUiStateType.Send -> if (isSuccess?.value == false) R.string.common_send else null
}
val iconRes = when (currentState.value) {
SendUiStateType.Amount,
@ -52,7 +52,7 @@ internal fun SendScreen(uiState: SendUiState) {
AppBarWithBackButtonAndIcon(
text = titleRes?.let { stringResource(it) },
onBackClick = uiState.clickIntents::onBackClick,
onBackClick = uiState.clickIntents::popBackStack,
onIconClick = uiState.clickIntents::onQrCodeScanClick,
backIconRes = R.drawable.ic_close_24,
iconRes = iconRes,
@ -94,7 +94,7 @@ private fun SendScreenContent(
uiState.feeState,
uiState.clickIntents,
)
else -> { /* [REDACTED_TODO_COMMENT]*/ }
SendUiStateType.Send -> SendContent(uiState)
}
}
}

View file

@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.fields.AmountVisualTransformation
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme

View file

@ -1,17 +1,21 @@
package com.tangem.features.send.impl.presentation.ui.fee
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.fields.AmountVisualTransformation
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.inputrow.InputRowEnter
import com.tangem.core.ui.components.inputrow.InputRowEnterInfo
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
private const val ETHEREUM_UNIT = "GWEI"
@ -22,42 +26,66 @@ internal fun SendCustomFeeEthereum(
symbol: String,
modifier: Modifier = Modifier,
) {
val fee = customValues.value[0]
val gasPrice = customValues.value[1]
val gasLimit = customValues.value[2]
if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) {
val fee = customValues.value[0]
val gasPrice = customValues.value[1]
val gasLimit = customValues.value[2]
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = modifier,
) {
TextFieldWithInfo(
value = fee.value,
label = stringResource(R.string.send_max_fee),
FooterContainer(
footer = stringResource(R.string.send_max_fee_footer),
info = fee.label,
visualTransformation = AmountVisualTransformation(symbol),
keyboardOptions = fee.keyboardOptions,
onValueChange = fee.onValueChange,
isSingleLine = true,
)
TextFieldWithInfo(
value = gasPrice.value,
label = stringResource(R.string.send_gas_price),
) {
InputRowEnterInfo(
text = fee.value,
title = TextReference.Res(R.string.send_max_fee),
info = fee.label,
visualTransformation = AmountVisualTransformation(symbol),
keyboardOptions = fee.keyboardOptions,
onValueChange = fee.onValueChange,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
FooterContainer(
footer = stringResource(R.string.send_gas_price_footer),
onValueChange = gasPrice.onValueChange,
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
)
TextFieldWithInfo(
value = gasLimit.value,
label = stringResource(R.string.send_gas_limit),
) {
InputRowEnter(
text = gasPrice.value,
title = TextReference.Res(R.string.send_gas_price),
onValueChange = gasPrice.onValueChange,
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
FooterContainer(
footer = stringResource(R.string.send_gas_limit_footer),
onValueChange = gasLimit.onValueChange,
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
)
) {
InputRowEnter(
text = gasLimit.value,
title = TextReference.Res(R.string.send_gas_limit),
onValueChange = gasLimit.onValueChange,
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}
}
}

View file

@ -18,11 +18,13 @@ import androidx.compose.ui.res.stringResource
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.inputrow.InputRowRecipient
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.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
@ -45,18 +47,26 @@ internal fun SendRecipientContent(
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
item(key = ADDRESS_FIELD_KEY) {
TextFieldWithPasteAndIcon(
value = address.value,
label = address.label,
placeholder = address.placeholder,
FooterContainer(
footer = stringResource(R.string.send_recipient_address_footer, uiState.network),
onValueChange = address.onValueChange,
onPasteClick = clickIntents::onRecipientAddressValueChange,
singleLine = true,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing4),
isError = address.isError,
error = address.error,
)
) {
InputRowRecipient(
value = address.value,
title = address.label,
placeholder = address.placeholder,
onValueChange = address.onValueChange,
onPasteClick = clickIntents::onRecipientAddressValueChange,
singleLine = true,
isError = address.isError,
error = address.error,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}
memo?.let { memoField ->
item(key = MEMO_FIELD_KEY) {

View file

@ -0,0 +1,70 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.inputrow.inner.PasteButton
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.ui.common.FooterContainer
@Composable
internal fun TextFieldWithPaste(
value: String,
placeholder: TextReference,
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
error: TextReference? = null,
isError: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
}
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 = title.resolveReference(),
style = TangemTheme.typography.body2,
color = color,
)
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),
)
}
}
}

View file

@ -1,385 +0,0 @@
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.foundation.text.KeyboardOptions
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.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
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
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
@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,
error: TextReference? = null,
isError: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
}
FooterContainer(modifier, footer) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = color,
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,
error: TextReference? = null,
isError: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
}
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 = title.resolveReference(),
style = TangemTheme.typography.body2,
color = color,
)
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,
isSingleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
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,
visualTransformation = visualTransformation,
singleLine = isSingleLine,
keyboardOptions = keyboardOptions,
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 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,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
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

@ -0,0 +1,193 @@
package com.tangem.features.send.impl.presentation.ui.send
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImage
import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
@Suppress("LongMethod")
@Composable
internal fun SendContent(uiState: SendUiState) {
val amountState = uiState.amountState ?: return
val recipientState = uiState.recipientState ?: return
val feeState = uiState.feeState ?: return
val sendState = uiState.sendState ?: return
val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle()
val timestamp = sendState.transactionDate.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AnimatedVisibility(visible = isSuccess.value) {
TransactionDoneTitle(
titleRes = R.string.sent_transaction_sent_title,
date = timestamp.value,
)
}
AnimatedVisibility(visible = !isSuccess.value) {
FromWallet(
walletName = amountState.walletName,
walletBalance = amountState.walletBalance,
)
}
AmountBlock(
amountState = amountState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showAmount,
)
RecipientBlock(
recipientState = recipientState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showRecipient,
)
FeeBlock(
feeState = feeState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showFee,
)
}
}
@Composable
private fun FromWallet(walletName: String, walletBalance: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.button.disabled)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = buildAnnotatedString {
append(stringResource(R.string.send_from_wallet_android))
append(" ")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(walletName)
}
},
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = walletBalance,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing8,
),
)
}
}
@Composable
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val amount = amountState.amountTextField.collectAsStateWithLifecycle()
val cryptoAmount = formatCryptoAmount(
cryptoCurrency = amountState.cryptoCurrencyStatus.currency,
cryptoAmount = amount.value.value.toBigDecimalOrDefault(),
)
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(),
fiatCurrencyCode = amountState.appCurrency.code,
fiatCurrencySymbol = amountState.appCurrency.symbol,
)
InputRowImage(
title = TextReference.Res(R.string.send_amount_label),
subtitle = TextReference.Str(cryptoAmount),
caption = TextReference.Str(fiatAmount),
tokenIconState = amountState.tokenIconState,
showNetworkIcon = true,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
)
}
@Composable
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val address = recipientState.addressTextField.collectAsStateWithLifecycle()
val memo = recipientState.memoTextField?.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
) {
val showMemo = memo != null && memo.value.value.isNotBlank()
InputRowRecipientDefault(
title = TextReference.Res(R.string.send_recipient),
value = address.value.value,
showDivider = showMemo,
)
if (showMemo) {
InputRowDefault(
title = TextReference.Res(R.string.send_extras_hint_memo),
text = TextReference.Str(memo?.value?.value.orEmpty()),
)
}
}
}
@Composable
private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val feeSelector =
feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return
val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0)
val feeValue = formatCryptoAmount(
cryptoCurrency = feeState.cryptoCurrencyStatus.currency,
cryptoAmount = when (val selectedFee = feeSelector.fees) {
is TransactionFee.Single -> selectedFee.normal.amount.value
is TransactionFee.Choosable -> when (feeSelector.selectedFee) {
FeeType.SLOW -> selectedFee.minimum.amount.value
FeeType.MARKET -> selectedFee.normal.amount.value
FeeType.FAST -> selectedFee.priority.amount.value
FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault()
}
},
)
InputRowDefault(
title = TextReference.Res(R.string.send_network_fee_title),
text = TextReference.Str(feeValue),
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
)
}

View file

@ -7,6 +7,7 @@ import java.math.BigInteger
internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean {
if (cryptoCurrency == null) return false
if (memo.isEmpty()) return true
return when (cryptoCurrency.network.id.value) {
Blockchain.XRP.id -> {
val tag = memo.toLongOrNull()

View file

@ -4,6 +4,8 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType
interface SendClickIntents {
fun popBackStack()
fun onBackClick()
fun onNextClick()
@ -33,4 +35,16 @@ interface SendClickIntents {
fun onSubtractSelect(value: Boolean)
// endregion
// region Send
fun onSendClick()
fun showAmount()
fun showRecipient()
fun showFee()
fun onExploreClick(txUrl: String)
// endregion
}

View file

@ -9,6 +9,7 @@ import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.Provider
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -18,8 +19,11 @@ import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -28,6 +32,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.SendStateFactory
import com.tangem.features.send.impl.presentation.state.SendUiState
@ -48,7 +53,7 @@ import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
@HiltViewModel
internal class SendViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
@ -60,6 +65,8 @@ internal class SendViewModel @Inject constructor(
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val walletManagersFacade: WalletManagersFacade,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
@ -73,7 +80,8 @@ internal class SendViewModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var innerRouter: StateRouter by Delegates.notNull()
private var inneRrouter: InnerSendRouter by Delegates.notNull()
private var stateRouter: StateRouter by Delegates.notNull()
private val stateFactory = SendStateFactory(
clickIntents = this,
@ -102,9 +110,10 @@ internal class SendViewModel @Inject constructor(
getFee()
}
fun setRouter(router: StateRouter) {
innerRouter = router
uiState = uiState.copy(currentState = router.currentState)
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
inneRrouter = router
this.stateRouter = stateRouter
uiState = uiState.copy(currentState = stateRouter.currentState)
}
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
@ -271,9 +280,10 @@ internal class SendViewModel @Inject constructor(
}
// region screen state navigation
override fun onBackClick() = innerRouter.onBackClick()
override fun onNextClick() = innerRouter.onNextClick()
override fun onPrevClick() = innerRouter.onPrevClick()
override fun popBackStack() = stateRouter.popBackStack()
override fun onBackClick() = stateRouter.onBackClick()
override fun onNextClick() = stateRouter.onNextClick()
override fun onPrevClick() = stateRouter.onPrevClick()
override fun onQrCodeScanClick() {
// TODO Add QR code scanning
@ -314,7 +324,7 @@ internal class SendViewModel @Inject constructor(
}
private fun checkIfXrpAddressValue(value: String): Boolean {
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) {
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) {
viewModelScope.launch(dispatchers.io) {
val result = XrpAddressService.decodeXAddress(value)
onRecipientAddressValueChange(result?.address.orEmpty())
@ -382,8 +392,108 @@ internal class SendViewModel @Inject constructor(
}
//endregion
// region send state clicks
override fun onSendClick() {
val sendState = uiState.sendState ?: return
if (sendState.isSuccess.value) popBackStack()
sendState.isSending.update { true }
viewModelScope.launch(dispatchers.io) {
verifyAndSendTransaction()
}
}
private suspend fun verifyAndSendTransaction() {
val sendState = uiState.sendState ?: return
val amount = uiState.amountState?.amountTextField?.value ?: return
val recipient = uiState.recipientState?.addressTextField?.value ?: return
val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return
val memo = uiState.recipientState?.memoTextField?.value
val fee = getFee(feeState) ?: return
val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency)
// todo add notifications [[REDACTED_JIRA]]
// val transactionErrors = walletManagersFacade.validateTransaction(
// amount = amountToSend,
// fee = fee.amount,
// userWalletId = userWalletId,
// network = cryptoCurrency.network,
// )
val txData = walletManagersFacade.createTransaction(
amount = amountToSend,
fee = fee,
memo = memo?.value,
destination = recipient.value,
userWalletId = userWalletId,
network = cryptoCurrency.network,
) ?: return
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = cryptoCurrency.network,
).fold(
ifLeft = {
sendState.isSending.update { false }
// todo add notifications [[REDACTED_JIRA]]
},
ifRight = {
sendState.transactionDate.update {
txData.date?.timeInMillis ?: System.currentTimeMillis()
}
sendState.isSuccess.update { true }
sendState.txUrl.update {
getTxUrl(txData.hash.orEmpty())
}
},
)
}
private fun getFee(feeState: FeeSelectorState.Content): Fee? {
return when (val selectedFee = feeState.fees) {
is TransactionFee.Choosable -> {
when (feeState.selectedFee) {
FeeType.SLOW -> selectedFee.minimum
FeeType.MARKET -> selectedFee.normal
FeeType.FAST -> selectedFee.priority
FeeType.CUSTOM -> {
val feeAmount = feeState.customValues.value.firstOrNull()?.value
?.let { BigDecimal(it) } ?: return null
Fee.Common(feeAmount.convertToAmount(cryptoCurrency))
}
}
}
is TransactionFee.Single -> selectedFee.normal
}
}
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
override fun showRecipient() = stateRouter.showRecipient(isFromSend = true)
override fun showFee() = stateRouter.showFee(isFromSend = true)
override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl)
private fun getTxUrl(hash: String): String {
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
EMPTY
} else {
getExplorerTransactionUrlUseCase(
txHash = hash,
networkId = cryptoCurrency.network.id,
)
}
}
// endregion
companion object {
private const val XRP_X_ADDRESS = 'X'
private const val DEFAULT_VALUE = "0.00"
private const val EMPTY = ""
}
}