Updated on 2026-08-14

This commit is contained in:
Tangem 2022-05-23 18:45:56 +03:00
commit 5082126440
37 changed files with 544 additions and 362 deletions

View file

@ -93,7 +93,8 @@ dependencies {
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-77'
implementation 'com.tangem:blockchain:develop-81'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_round
Blockchain.Litecoin -> R.drawable.ic_litecoin_round
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth_round
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> R.drawable.ic_eth_round
Blockchain.RSK -> R.drawable.ic_rsk_round
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_round
Blockchain.Tezos -> R.drawable.ic_tezos_round
@ -36,6 +37,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_no_color
Blockchain.Litecoin -> R.drawable.ic_litecoin_no_color
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth_no_color
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet, -> R.drawable.ic_eth_no_color
Blockchain.RSK -> R.drawable.ic_rsk_no_color
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_no_color
Blockchain.Tezos -> R.drawable.ic_tezos_no_color

View file

@ -28,6 +28,7 @@ private class AddCustomTokenConverter(
val rawMessage = when (customTokenError) {
AddCustomTokenError.Warning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenError.Warning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.Warning.UnsupportedSolanaToken -> R.string.alert_manage_tokens_unsupported_message
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path

View file

@ -0,0 +1,72 @@
package com.tangem.tap.common.recyclerView
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tangem_sdk_new.extensions.dpToPx
class SpaceItemDecoration(
private val horizontalSpaceDp: Float,
private val verticalSpaceDp: Float,
) : RecyclerView.ItemDecoration() {
private lateinit var space: Space
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (state.itemCount == 0) return
if (!::space.isInitialized) {
space = Space(
view.dpToPx(horizontalSpaceDp).toInt(),
view.dpToPx(verticalSpaceDp).toInt()
)
}
outRect.left = space.horizontal
outRect.right = space.horizontal
when (state.itemCount) {
1 -> {
outRect.top = space.vertical
outRect.bottom = space.vertical
}
else -> {
val adapterPosition = parent.getChildAdapterPosition(view)
if (adapterPosition == -1) return
when (adapterPosition) {
0 -> {
// first
outRect.top = space.vertical
outRect.bottom = space.vertical / 2
}
state.itemCount - 1 -> {
// last
outRect.top = space.vertical / 2
outRect.bottom = space.vertical
}
else -> {
// middle
outRect.top = space.vertical / 2
outRect.bottom = space.vertical / 2
}
}
}
}
}
private data class Space(
val horizontal: Int,
val vertical: Int,
)
companion object {
fun all(dp: Float): SpaceItemDecoration = SpaceItemDecoration(dp, dp)
fun vertical(dp: Float): SpaceItemDecoration = SpaceItemDecoration(0f, dp)
fun horizontal(dp: Float): SpaceItemDecoration = SpaceItemDecoration(dp, 0f)
}
}

View file

@ -14,13 +14,13 @@ import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
@ -36,7 +36,6 @@ import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
@ -244,7 +243,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
warningsAdapter = WarningMessagesAdapter()
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt()))
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
store.dispatch(SendAction.Warnings.Update)

View file

@ -58,8 +58,7 @@ class TokensMiddleware {
currenciesRepository.getSupportedTokens(isTestcard)
.filter(action.supportedBlockchains?.toSet())
}
val delay = async { delay(600) }
delay.await()
delay(600)
store.dispatchOnMain(TokensAction.LoadCurrencies.Success(currencies.await()))
}
}
@ -78,8 +77,8 @@ class TokensMiddleware {
val blockchainsToRemove = currentBlockchains.filter { !action.addedBlockchains.contains(it) }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
val tokensToRemove = currentTokens.filter {
token -> !action.addedTokens.any { it.token == token.token }
val tokensToRemove = currentTokens.filter { token ->
!action.addedTokens.any { it.token == token.token }
}
val derivationStyle = scanResponse.card.derivationStyle

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.wallet.redux.WalletData
@ -19,7 +20,12 @@ data class TokensState(
val allowToAdd: Boolean = true,
val derivationStyle: DerivationStyle? = null,
val scanResponse: ScanResponse? = null
) : StateType
) : StateType {
fun canHandleToken(token: TokenWithBlockchain): Boolean {
return scanResponse?.card?.canHandleToken(token.blockchain) ?: false
}
}
typealias ContractAddress = String
@ -62,7 +68,7 @@ fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency
it.copy(contracts =
it.contracts.filter {
supportedBlockchains.contains(it.blockchain) &&
(it.blockchain.canHandleTokens() || it.address == null)
(it.blockchain.canHandleTokens() || it.address == null)
}
)
}.filterNot {

View file

@ -22,7 +22,9 @@ import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.pixelsToDp
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@ -37,7 +39,7 @@ fun CurrenciesScreen(
onSaveChanges: (List<TokenWithBlockchain>, List<Blockchain>) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
val context = LocalContext.current
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
@ -77,7 +79,7 @@ fun CurrenciesScreen(
visible = tokensState.value.currencies.isEmpty(),
enter = fadeIn(),
exit = fadeOut()
){
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
@ -93,8 +95,9 @@ fun CurrenciesScreen(
exit = fadeOut(animationSpec = tween(1000))
) {
Column {
if (tokensState.value.scanResponse?.card?.useOldStyleDerivation == true) CurrenciesWarning()
val showHeader = tokensState.value.scanResponse?.card?.useOldStyleDerivation == true
ListOfCurrencies(
header = { if (showHeader) CurrenciesWarning() },
currencies = tokensState.value.currencies,
nonRemovableTokens = tokensState.value.nonRemovableTokens,
nonRemovableBlockchains = tokensState.value.nonRemovableBlockchains,
@ -102,7 +105,19 @@ fun CurrenciesScreen(
addedBlockchains = addedBlockchainsState.value,
searchInput = searchInput.value,
allowToAdd = tokensState.value.allowToAdd,
onAddCurrencyToggled = onAddCurrencyToggleClick,
onAddCurrencyToggled = { currency, token ->
onAddCurrencyToggleClick(currency, token)
token?.let {
if (!tokensState.value.canHandleToken(it)) {
val dialog = AppDialog.SimpleOkDialog(
header = context.getString(R.string.common_warning),
message = context.getString(R.string.alert_manage_tokens_unsupported_message)
) { onAddCurrencyToggleClick(currency, it) }
store.dispatchDialogShow(dialog)
}
}
},
onNetworkItemClicked = onNetworkItemClicked
)
}

View file

@ -17,6 +17,7 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@Composable
fun ListOfCurrencies(
header: @Composable ()->Unit,
currencies: List<Currency>,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
@ -54,6 +55,7 @@ fun ListOfCurrencies(
.contains(searchInput)
}.toList()
}
item { header() }
items(filteredCurrencies) { currency ->
CurrencyItem(
currency = currency,

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.wallet.models
sealed class WalletWarning(
val showingPosition: Int,
) {
object TransactionInProgress : WalletWarning(10)
object SolanaTokensUnsupported : WalletWarning(20)
data class BalanceNotEnoughForFee(val blockchainFullName: String) : WalletWarning(30)
data class Rent(val walletRent: WalletRent) : WalletWarning(40)
}
data class WalletWarningDescription(
val title: String,
val message: String,
)
data class WalletRent(
val minRentValue: String,
val rentExemptValue: String,
)

View file

@ -157,10 +157,10 @@ sealed class WalletAction : Action {
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class SetWalletRent(
val blockchain: BlockchainNetwork,
val wallet: Wallet,
val minRent: String,
val rentExempt: String
) : WalletAction()
data class RemoveWalletRent(val blockchain: BlockchainNetwork) : WalletAction()
data class RemoveWalletRent(val wallet: Wallet) : WalletAction()
}

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.entities.Button
@ -19,10 +20,7 @@ import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -357,31 +355,44 @@ data class WalletData(
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency,
val warningRent: WalletRent? = null,
val walletRent: WalletRent? = null,
) {
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false
return listOfAddresses.size > 1
}
fun shouldShowCoinAmountWarning(): Boolean = when (currency) {
is Currency.Blockchain -> false
is Currency.Token -> blockchainAmountIsEmpty() && !tokenAmountIsEmpty()
}
fun shouldEnableTokenSendButton(): Boolean = !blockchainAmountIsEmpty() || !tokenAmountIsEmpty()
private fun blockchainAmountIsEmpty(): Boolean =
currencyData.blockchainAmount?.isZero() ?: false
fun assembleWarnings(): List<WalletWarning> {
val blockchain = currency.blockchain
val walletWarnings = mutableListOf<WalletWarning>()
if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress)
}
if (currency.isBlockchain()) {
if (blockchain == Blockchain.Solana || blockchain == Blockchain.SolanaTestnet) {
val card = store.state.globalState.scanResponse?.card
if (card?.canHandleToken(blockchain) == false) {
walletWarnings.add(WalletWarning.SolanaTokensUnsupported)
}
}
}
if (walletRent != null) {
walletWarnings.add(WalletWarning.Rent(walletRent))
}
if (!currency.isBlockchain() && (blockchainAmountIsEmpty() && !tokenAmountIsEmpty())) {
val fullName = currency.blockchain.fullName
walletWarnings.add(WalletWarning.BalanceNotEnoughForFee(fullName))
}
return walletWarnings.sortedBy { it.showingPosition }
}
private fun blockchainAmountIsEmpty(): Boolean = currencyData.blockchainAmount?.isZero() ?: false
private fun tokenAmountIsEmpty(): Boolean = currencyData.amount?.isZero() == true
}
data class WalletRent(
val minRentValue: String,
val rentExemptValue: String
)
sealed interface Currency {
val coinId: String?

View file

@ -24,7 +24,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
@ -330,14 +329,12 @@ class WalletMiddleware {
val currency = walletManager.wallet.blockchain.currency
if (show) {
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
wallet = walletManager.wallet,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
))
dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet))
}
}
is com.tangem.blockchain.extensions.Result.Failure -> {}

View file

@ -14,6 +14,7 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
@ -148,12 +149,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
val currencies = listOf(Currency.fromBlockchainNetwork(action.blockchain)) +
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(
action.blockchain,
it
)
}
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(action.blockchain, it)
}
val newWallets = newState.walletsData.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
@ -307,21 +305,15 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.SetWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData != null) {
walletData = walletData.copy(
warningRent = WalletRent(action.minRent, action.rentExempt)
)
newState = newState.updateWalletsData(listOf(walletData))
}
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletRent = WalletRent(action.minRent, action.rentExempt)
val walletsData = walletStore.walletsData.map { it.copy(walletRent = walletRent) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.RemoveWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData != null) {
walletData = walletData.copy(warningRent = null)
newState = newState.updateWalletsData(listOf(walletData))
}
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletsData = walletStore.walletsData.map { it.copy(walletRent = null) }
newState = newState.updateWalletsData(walletsData)
}
else -> { /* no-op */
}

View file

@ -13,10 +13,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.common.extensions.guard
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.BlockchainNetwork
@ -24,6 +24,7 @@ import com.tangem.tap.features.onboarding.getQRReceiveMessage
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
import com.tangem.tap.store
@ -35,6 +36,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
StoreSubscriber<WalletState> {
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter
private var dialog: Dialog? = null
private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind)
@ -72,6 +74,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
setupTransactionsRecyclerView()
setupButtons()
setupWarningsRecyclerView()
setupTestActionButton()
}
@ -81,6 +84,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
rvPendingTransaction.adapter = pendingTransactionAdapter
}
private fun setupWarningsRecyclerView() = with(binding) {
warningMessagesAdapter = WalletDetailWarningMessagesAdapter()
rvWarningMessages.layoutManager = LinearLayoutManager(requireContext())
rvWarningMessages.adapter = warningMessagesAdapter
rvWarningMessages.addItemDecoration(SpaceItemDecoration.vertical(8f))
}
private fun setupButtons() = with(binding) {
btnConfirm.text = getString(R.string.wallet_button_send)
btnConfirm.setOnClickListener { store.dispatch(WalletAction.Send()) }
@ -116,8 +126,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
handleDialogs(state.walletDialog)
handleCurrencyIcon(selectedWallet)
handleWalletRent(selectedWallet.warningRent)
handleNotEnoughFundsOnMainCurrency(selectedWallet)
handleWarnings(selectedWallet)
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
@ -162,31 +171,12 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
btnSell.show(selectedWallet.tradeCryptoState.sellingAllowed)
}
private fun handleWalletRent(rent: WalletRent?) = with(binding) {
val rent = rent.guard {
lWarning.root.hide()
return
}
val warningMessage = requireContext().getString(
R.string.solana_rent_warning, rent.minRentValue, rent.rentExemptValue
)
lWarning.tvWarningMessage.text = warningMessage
lWarning.root.show()
}
private fun handleWarnings(selectedWallet: WalletData) = with(binding) {
val converter = WalletWarningConverter(requireContext())
val warningDetails = selectedWallet.assembleWarnings().map { converter.convert(it) }
private fun handleNotEnoughFundsOnMainCurrency(selectedWalletData: WalletData) = with(binding) {
if (selectedWalletData.currency.isBlockchain()) return@with
if (selectedWalletData.shouldShowCoinAmountWarning()) {
val blockchainName = selectedWalletData.currency.blockchain.fullName
val warningMessage = requireContext().getString(
R.string.token_details_send_blocked_fee_format, blockchainName, blockchainName
)
lWarning.tvWarningMessage.text = warningMessage
lWarning.root.show()
} else {
lWarning.root.hide()
}
warningMessagesAdapter.submitList(warningDetails)
rvWarningMessages.show(warningDetails.isNotEmpty())
}
private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) {
@ -299,7 +289,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
)
}
}
binding.cardPendingTransactionWarning.show(data.status == BalanceStatus.SameCurrencyTransactionInProgress)
}
private fun handleDialogs(walletDialog: StateDialog?) {

View file

@ -13,9 +13,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -25,7 +25,6 @@ import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
@ -100,7 +99,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
with(binding) {
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt()))
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.features.wallet.ui
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.models.WalletWarningDescription
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class WalletWarningConverter(
private val context: Context,
) : ModuleMessageConverter<WalletWarning, WalletWarningDescription> {
override fun convert(message: WalletWarning): WalletWarningDescription {
val warningMessage = when (message) {
is WalletWarning.BalanceNotEnoughForFee -> {
context.getString(
R.string.token_details_send_blocked_fee_format,
message.blockchainFullName, message.blockchainFullName
)
}
WalletWarning.SolanaTokensUnsupported -> {
context.getString(R.string.warning_token_send_unsupported_message)
}
WalletWarning.TransactionInProgress -> {
context.getString(R.string.wallet_pending_transaction_warning)
}
is WalletWarning.Rent -> {
context.getString(
R.string.solana_rent_warning,
message.walletRent.minRentValue, message.walletRent.rentExemptValue
)
}
}
return WalletWarningDescription(context.getString(R.string.common_warning), warningMessage)
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.features.wallet.models.WalletWarningDescription
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutWarningCardBinding
/**
[REDACTED_AUTHOR]
*/
class WalletDetailWarningMessagesAdapter
: ListAdapter<WalletWarningDescription, WalletDetailsWarningMessageVH>(DiffUtilCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletDetailsWarningMessageVH {
val inflater = LayoutInflater.from(parent.context)
val binding = LayoutWarningCardBinding.inflate(inflater, parent, false)
return WalletDetailsWarningMessageVH(binding)
}
override fun onBindViewHolder(holder: WalletDetailsWarningMessageVH, position: Int) {
holder.bind(currentList[position])
}
private class DiffUtilCallback : DiffUtil.ItemCallback<WalletWarningDescription>() {
override fun areContentsTheSame(oldItem: WalletWarningDescription, newItem: WalletWarningDescription) =
oldItem == newItem
override fun areItemsTheSame(oldItem: WalletWarningDescription, newItem: WalletWarningDescription) =
oldItem == newItem
}
}
class WalletDetailsWarningMessageVH(
val binding: LayoutWarningCardBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(warning: WalletWarningDescription) {
binding.warningCard.setCardBackgroundColor(binding.root.getColor(R.color.darkGray2))
setText(warning)
}
private fun setText(warning: WalletWarningDescription) = with(binding.warningContentContainer) {
tvTitle.text = warning.title
tvMessage.text = warning.message
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.content.res.Resources
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -9,7 +8,6 @@ import androidx.core.os.ConfigurationCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.*
@ -20,13 +18,13 @@ import com.tangem.tap.features.feedback.RateCanBeBetterEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutWarningBinding
import com.tangem.wallet.databinding.LayoutWarningCardActionBinding
import timber.log.Timber
class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(DiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH {
val binding = LayoutWarningBinding.inflate(
val binding = LayoutWarningCardActionBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return WarningMessageVH(binding)
@ -45,7 +43,7 @@ class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(Dif
}
}
class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHolder(binding.root) {
class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(warning: WarningMessage) {
setBgColor(warning.priority)
@ -53,7 +51,7 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
setupControlButtons(warning)
}
private fun setText(warning: WarningMessage) = with(binding) {
private fun setText(warning: WarningMessage) = with(binding.warningContentContainer) {
fun getString(resId: Int?, default: String, formatArgs: String? = null) =
if (resId == null) default else root.getString(resId, formatArgs)
@ -71,7 +69,7 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
WarningMessage.Priority.Warning -> R.color.warning_warning
WarningMessage.Priority.Critical -> R.color.warning_critical
}
binding.cardView.setCardBackgroundColor(binding.root.getColor(color))
binding.warningCardAction.setCardBackgroundColor(binding.root.getColor(color))
}
private fun setupControlButtons(warning: WarningMessage) = when (warning.type) {
@ -115,7 +113,7 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
val buttonTitle = binding.root.getString(
warning.buttonTextId ?: R.string.how_to_got_it_button
)
binding.btnGotIt.setOnClickListener (buttonAction)
binding.btnGotIt.setOnClickListener(buttonAction)
binding.btnGotIt.text = buttonTitle
}
WarningMessage.Type.AppRating -> {
@ -161,19 +159,4 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
}
}
}
}
class SpacesItemDecoration(private val spacePx: Int) : ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.left = spacePx
outRect.right = spacePx
outRect.top = spacePx / 2
outRect.top = spacePx / 2
}
}

View file

@ -98,48 +98,22 @@
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<include
android:id="@+id/l_warning"
layout="@layout/layout_wallet_details_warning"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_warning_messages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/l_wallet_details" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_pending_transaction_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="@color/darkGray1"
app:cardCornerRadius="8dp"
app:layout_constraintTop_toBottomOf="@+id/l_wallet_details">
<TextView
android:id="@+id/tv_pending_transaction_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/wallet_pending_transaction_warning"
android:textColor="@android:color/white"
android:textSize="14sp" />
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="l_wallet_details,card_pending_transaction_warning" />
app:constraint_referenced_ids="l_wallet_details" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_left_button"
@ -168,7 +142,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_sell"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/l_warning"
app:layout_constraintTop_toBottomOf="@+id/rv_warning_messages"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
@ -185,7 +159,7 @@
app:layout_constraintEnd_toStartOf="@id/btn_confirm"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/btn_trade"
app:layout_constraintTop_toBottomOf="@+id/l_warning"
app:layout_constraintTop_toBottomOf="@+id/rv_warning_messages"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
@ -201,7 +175,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_sell"
app:layout_constraintTop_toBottomOf="@+id/l_warning"
app:layout_constraintTop_toBottomOf="@+id/rv_warning_messages"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="3dp"
app:cardBackgroundColor="@color/darkGray2">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_warning_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/common_warning"
android:textColor="@color/lightGray0"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_warning_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:lineSpacingExtra="4dp"
android:textColor="@color/lightGray0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_warning_title"
tools:text="@string/solana_rent_warning" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,110 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/warning_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:cardBackgroundColor="@color/accent"
app:cardCornerRadius="8dp">
android:background="@android:color/transparent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent">
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Test title" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text="Test title"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/btn_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textColor="@android:color/white"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
app:layout_constraintVertical_bias="0.0"
app:lineHeight="18dp"
tools:text="@string/lorem_ipsum" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:tint="@android:color/white"
app:layout_constraintBottom_toBottomOf="@+id/tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_title"
app:srcCompat="@drawable/ic_close" />
<TextView
android:id="@+id/tv_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="Test text messaging, or texting, is the act of composing and sending electronic messages, typically consisting of alphabetic and numeric characters, between"
android:textColor="@android:color/white"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
app:layout_constraintVertical_bias="0.0"
app:lineHeight="18dp" />
<Button
android:id="@+id/btn_got_it"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/how_to_got_it_button"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<Button
android:id="@+id/btn_can_be_better"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/warning_button_can_be_better"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@+id/btn_really_cool"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<Button
android:id="@+id/btn_really_cool"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/warning_button_really_cool"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_temporary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="btn_got_it" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="btn_can_be_better, btn_really_cool" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/warning_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="3dp"
tools:cardBackgroundColor="@color/darkGray2">
<include
android:id="@+id/warning_content_container"
layout="@layout/layout_warning" />
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/warning_card_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:cardCornerRadius="8dp"
tools:cardBackgroundColor="@color/accent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:layout_marginEnd="12dp"
android:tint="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/warning_content_container"
app:srcCompat="@drawable/ic_close" />
<include
android:id="@+id/warning_content_container"
layout="@layout/layout_warning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_got_it"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-16dp"
android:text="@string/how_to_got_it_button"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/warning_content_container" />
<Button
android:id="@+id/btn_can_be_better"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-16dp"
android:text="@string/warning_button_can_be_better"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@+id/btn_really_cool"
app:layout_constraintTop_toBottomOf="@+id/warning_content_container" />
<Button
android:id="@+id/btn_really_cool"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-16dp"
android:text="@string/warning_button_really_cool"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/warning_content_container" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_temporary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="btn_got_it" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="btn_can_be_better, btn_really_cool" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View file

@ -39,6 +39,8 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in Solana network are not supported by this card due to firmware limitation.</string>
<string name="warning_token_send_unsupported_message">Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>

View file

@ -39,6 +39,8 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in Solana network are not supported by this card due to firmware limitation.</string>
<string name="warning_token_send_unsupported_message">Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>

View file

@ -39,6 +39,8 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in Solana network are not supported by this card due to firmware limitation.</string>
<string name="warning_token_send_unsupported_message">Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>

View file

@ -39,6 +39,8 @@
<string name="currency_subtitle_expanded">Доступные сети</string>
<string name="alert_manage_tokens_addresses_message">Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства.</string>
<string name="alert_manage_tokens_unsupported_message">Токены в сети Solana не поддерживаются этой картой из-за ограничений прошивки.</string>
<string name="warning_token_send_unsupported_message">Не осуществляйте перевод на токены в данной сети иначе это может привести к их безвозвратной утере.</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="alert_funds_restoration_message">Если вы совершили ошибку с выбором сети при переводе средств с биржи, эта инструкция поможет вам восстановить средства</string>

View file

@ -39,6 +39,8 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in Solana network are not supported by this card due to firmware limitation.</string>
<string name="warning_token_send_unsupported_message">Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>

View file

@ -59,8 +59,8 @@ dependencies {
implementation implementation(project(path: ':network'))
implementation implementation(project(path: ':common'))
// Tangem sdk's
implementation 'com.tangem:blockchain:develop-78'
implementation 'com.tangem:blockchain:develop-81'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'

View file

@ -42,5 +42,6 @@ sealed class AddCustomTokenError(
sealed class Warning : AddCustomTokenError() {
object PotentialScamToken : Warning()
object TokenAlreadyAdded : Warning()
object UnsupportedSolanaToken: Warning()
}
}

View file

@ -12,6 +12,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"binance-smart-chain/test" -> Blockchain.BSCTestnet
"ethereum" -> Blockchain.Ethereum
"ethereum/test" -> Blockchain.EthereumTestnet
"ethereum-classic" -> Blockchain.EthereumClassic
"ethereum-classic/test" -> Blockchain.EthereumClassicTestnet
"polygon-pos", "matic-network" -> Blockchain.Polygon
"polygon-pos/test", "matic-network/test" -> Blockchain.PolygonTestnet
"solana" -> Blockchain.Solana
@ -54,6 +56,8 @@ fun Blockchain.toNetworkId(): String {
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum/test"
Blockchain.EthereumClassic -> "ethereum-classic"
Blockchain.EthereumClassicTestnet -> "ethereum-classic/test"
Blockchain.Fantom -> "fantom"
Blockchain.FantomTestnet -> "fantom/test"
Blockchain.Litecoin -> "litecoin"
@ -75,6 +79,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "bitcoin"
Blockchain.BitcoinCash, Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ethereum"
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "ethereum-classic"
Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar"
Blockchain.Cardano, Blockchain.CardanoShelley -> "cardano"
Blockchain.Polygon, Blockchain.PolygonTestnet -> "matic-network"

View file

@ -1,12 +1,50 @@
package com.tangem.domain.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.TapWorkarounds.isTestCard
/**
[REDACTED_AUTHOR]
*/
val FirmwareVersion.Companion.SolanaAvailable
get() = FirmwareVersion(4, 12)
val FirmwareVersion.Companion.SolanaTokensAvailable
get() = FirmwareVersion(4, 52)
get() = FirmwareVersion(4, 52)
fun Card.supportedBlockchains(): List<Blockchain> {
val supportedBlockchains = when {
firmwareVersion < FirmwareVersion.MultiWalletAvailable -> {
Blockchain.fromCurve(EllipticCurve.Secp256k1)
}
else -> {
Blockchain.fromCurve(EllipticCurve.Secp256k1) +
Blockchain.fromCurve(EllipticCurve.Ed25519)
}
}
val filtered = supportedBlockchains.filter { isTestCard == it.isTestnet() }
return filtered
}
fun Card.supportedTokens(): List<Blockchain> {
val tokensSupportedByBlockchain = supportedBlockchains().filter { it.canHandleTokens() }.toMutableList()
val tokensSupportedByCard = when {
firmwareVersion >= FirmwareVersion.SolanaTokensAvailable -> tokensSupportedByBlockchain
else -> {
tokensSupportedByBlockchain.apply {
remove(Blockchain.Solana)
remove(Blockchain.SolanaTestnet)
}
}
}
val filtered = tokensSupportedByCard.filter { isTestCard == it.isTestnet() }
return filtered
}
fun Card.canHandleBlockchain(blockchain: Blockchain): Boolean {
return this.supportedBlockchains().contains(blockchain)
}
fun Card.canHandleToken(blockchain: Blockchain): Boolean {
return this.supportedTokens().contains(blockchain)
}

View file

@ -11,10 +11,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
/**
[REDACTED_AUTHOR]
*/
enum class CompleteDataType {
Blockchain, Token
}
sealed class CustomCurrency(
val network: Blockchain,
val derivationPath: DerivationPath?,

View file

@ -12,7 +12,9 @@ import com.tangem.domain.AddCustomTokenException
import com.tangem.domain.DomainDialog
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.form.*
import com.tangem.domain.features.addCustomToken.*
@ -141,16 +143,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
}
}
//TODO: Solana token
/**
* This feature is only needed until Solana coins are added.
* While they are not there - this function excludes the Solana blockchain if the user has
* filled in at least one field of the token.
*/
private suspend fun changeBlockchainNetworkList() {
val state = hubState
val networkBlockchainList = Network.getField<TokenBlockchainField>().itemList
val newNetworkBlockchainList: List<Blockchain> = state.getNetworks(state.getCustomTokenType())
val card = requireNotNull(globalState.scanResponse?.card)
val newNetworkBlockchainList: List<Blockchain> = state.getNetworks(card, state.getCustomTokenType())
val listsIdentical = newNetworkBlockchainList.toSet() == networkBlockchainList.toSet()
if (listsIdentical) return
@ -230,6 +227,15 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
val singleTokenContract = foundToken.networks[0]
fillTokenFields(foundToken, singleTokenContract)
if (canHandleToken(Network.getFieldValue())) {
AddCustomTokenError.Warning.UnsupportedSolanaToken.remove()
} else {
AddCustomTokenError.Warning.UnsupportedSolanaToken.add()
updateTokenDetailFields(false)
updateAddButton(false)
return
}
val isInAppSavedTokens = isTokenPersistIntoAppSavedTokensList()
if (isInAppSavedTokens) {
updateTokenDetailFields(false)
@ -515,6 +521,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
}
private fun canHandleToken(blockchain: Blockchain): Boolean {
val card = globalState.scanResponse?.card ?: return false
return card.canHandleToken(blockchain)
}
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw AddCustomTokenException.UnAppropriateInitializationException(
@ -537,9 +548,9 @@ private class AddCustomTokenReducer(
}
is OnCreate -> {
val card = requireNotNull(globalState.scanResponse?.card)
val supportedTokenNetworkIds = AddCustomTokenState.getSupportedTokensBlockchain().map {
it.toNetworkId()
}
val supportedTokenNetworkIds = card.supportedBlockchains()
.filter { it.canHandleTokens() }
.map { it.toNetworkId() }
val tangemTechServiceManager = AddCustomTokenService(
tangemTechService = globalState.networkServices.tangemTechService,
supportedTokenNetworkIds = supportedTokenNetworkIds
@ -550,7 +561,7 @@ private class AddCustomTokenReducer(
DerivationStyle.LEGACY -> derivationPathState.copy(isVisible = true)
null, DerivationStyle.NEW -> derivationPathState.copy(isVisible = false)
}
val form = Form(AddCustomTokenState.createFormFields(CustomTokenType.Blockchain))
val form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain))
state.copy(
cardDerivationStyle = card.derivationStyle,
form = form,
@ -558,7 +569,10 @@ private class AddCustomTokenReducer(
screenState = state.screenState.copy(derivationPath = derivationPathState)
)
}
is OnDestroy -> state.reset()
is OnDestroy -> {
val card = requireNotNull(globalState.scanResponse?.card)
state.reset(card)
}
is UpdateForm -> {
updateFormState(action.state)
}

View file

@ -2,17 +2,17 @@ package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.card.Card
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.extensions.SolanaAvailable
import com.tangem.domain.common.extensions.SolanaTokensAvailable
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.extensions.supportedTokens
import com.tangem.domain.common.form.*
import com.tangem.domain.features.addCustomToken.*
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.state.StringActionStateConverter
import org.rekotlin.Action
import org.rekotlin.StateType
@ -109,12 +109,12 @@ data class AddCustomTokenState(
null
}
fun reset(): AddCustomTokenState {
fun reset(card: Card): AddCustomTokenState {
return this.copy(
appSavedCurrencies = null,
onTokenAddCallback = null,
cardDerivationStyle = null,
form = Form(createFormFields(CustomTokenType.Blockchain)),
form = Form(createFormFields(card, CustomTokenType.Blockchain)),
formErrors = emptyMap(),
tokenId = null,
warnings = emptySet(),
@ -135,8 +135,8 @@ data class AddCustomTokenState(
.getConvertedData()
}
fun getNetworks(type: CustomTokenType): List<Blockchain> {
return getNetworksList(type)
fun getNetworks(card: Card, type: CustomTokenType): List<Blockchain> {
return getNetworksList(card, type)
}
companion object {
@ -154,66 +154,45 @@ data class AddCustomTokenState(
else -> derivationNetwork
}.derivationPath(derivationStyle)
internal fun createFormFields(type: CustomTokenType): List<DataField<*>> {
internal fun createFormFields(card: Card, type: CustomTokenType): List<DataField<*>> {
return listOf(
TokenField(ContractAddress),
TokenBlockchainField(Network, getNetworksList(type)),
TokenBlockchainField(Network, getNetworksList(card, type)),
TokenField(Name),
TokenField(Symbol),
TokenField(Decimals),
TokenDerivationPathField(DerivationPath, getSupportedDerivations()),
TokenDerivationPathField(DerivationPath, getSupportedDerivations(card)),
)
}
/**
* Serves to determine the tokens that can be received from the TangemTech service
*/
internal fun getSupportedTokensBlockchain(): List<Blockchain> {
return Blockchain.values()
.filter { !it.isTestnet() }
.filter { it.canHandleTokens() }
}
/**
* Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks.
* Blockchain.Unknown - is the default selection
*/
private fun getNetworksList(type: CustomTokenType): List<Blockchain> {
private fun getNetworksList(card: Card, type: CustomTokenType): List<Blockchain> {
val evmBlockchains = Blockchain.values()
.filter { it.isEvm() }
.filter { !it.isTestnet() }
.filter { card.isTestCard == it.isTestnet() }
val additionalBlockchains = listOf(
Blockchain.Binance,
Blockchain.Solana,
Blockchain.Binance, Blockchain.BinanceTestnet,
Blockchain.Solana, Blockchain.SolanaTestnet,
)
val networks = (evmBlockchains + additionalBlockchains).toMutableList()
//life hack
val fwCardVersion = domainStore.state.globalState.scanResponse?.card?.firmwareVersion
?: FirmwareVersion(0, 0)
val solanaUnsupportedByCard = fwCardVersion < FirmwareVersion.SolanaAvailable
val solanaTokensUnsupportedByCard = fwCardVersion < FirmwareVersion.SolanaTokensAvailable
if (solanaUnsupportedByCard || solanaTokensUnsupportedByCard) networks.remove(Blockchain.Solana)
if (type == CustomTokenType.Token) networks.removeAll(getUnsupportedTokensBlockchain())
val supportedByCard = when (type) {
CustomTokenType.Blockchain -> card.supportedBlockchains()
CustomTokenType.Token -> card.supportedTokens()
}
val typedNetworksList = (evmBlockchains + additionalBlockchains)
.filter { supportedByCard.contains(it) }
.toMutableList()
val default = Blockchain.Unknown
networks.add(0, default)
typedNetworksList.add(0, default)
return networks
return typedNetworksList
}
private fun getUnsupportedTokensBlockchain(): List<Blockchain> {
return Blockchain.values()
.filter { !it.isTestnet() }
.filter { !it.canHandleTokens() }
.toMutableList()
}
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<out Any>> {
return mapOf(
ContractAddress to TokenContractAddressValidator(),
@ -224,9 +203,9 @@ data class AddCustomTokenState(
)
}
private fun getSupportedDerivations(): List<Blockchain> {
private fun getSupportedDerivations(card: Card): List<Blockchain> {
val evmBlockchains = Blockchain.values().filter {
!it.isTestnet() && it.getChainId() != null
card.isTestCard == it.isTestnet() && it.getChainId() != null
}
return listOf(Blockchain.Unknown) + evmBlockchains
}