Updated on 2026-08-14

This commit is contained in:
Tangem 2022-03-29 21:01:29 +04:00
parent 040f53e52e
commit 2f1e813058
74 changed files with 1731 additions and 22861 deletions

View file

@ -0,0 +1,37 @@
package com.tangem.tap.common.compose
import android.graphics.Rect
import android.view.ViewTreeObserver
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView
sealed class Keyboard {
data class Opened(val height: Int): Keyboard()
object Closed : Keyboard()
}
@Composable
fun keyboardAsState(): State<Keyboard> {
val keyboardState: MutableState<Keyboard> = remember { mutableStateOf(Keyboard.Closed) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
Keyboard.Opened(keypadHeight)
} else {
Keyboard.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}

View file

@ -21,4 +21,59 @@ fun Blockchain.getIconRes(): Int {
Blockchain.Stellar -> R.drawable.ic_stellar
else -> R.drawable.shape_circle
}
}
@DrawableRes
fun Blockchain.getRoundIconRes(): Int {
return when (this) {
// Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet,-> R.drawable.ic_bitcoin_round
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.RSK -> R.drawable.ic_rsk_round
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_round
Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_binance_round
Blockchain.Tezos -> R.drawable.ic_tezos_round
Blockchain.XRP -> R.drawable.ic_xrp_round
Blockchain.Stellar -> R.drawable.ic_stellar_round
Blockchain.Avalanche, Blockchain.AvalancheTestnet -> R.drawable.ic_avalanche_round
Blockchain.Polygon, Blockchain.PolygonTestnet -> R.drawable.ic_polygon_round
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_round
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_round
Blockchain.BSC, Blockchain.BSCTestnet -> R.drawable.ic_bsc_round
else -> R.drawable.ic_tangem_logo
}
}
@DrawableRes
fun Blockchain.getGreyedOutIconRes(): Int {
return when (this) {
// Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet,-> R.drawable.ic_bitcoin_no_color
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.RSK -> R.drawable.ic_rsk_no_color
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_no_color
Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_binance_no_color
Blockchain.Tezos -> R.drawable.ic_tezos_no_color
Blockchain.XRP -> R.drawable.ic_xrp_no_color
Blockchain.Stellar -> R.drawable.ic_stellar_no_color
Blockchain.Avalanche, Blockchain.AvalancheTestnet -> R.drawable.ic_avalanche_no_color
Blockchain.Polygon, Blockchain.PolygonTestnet -> R.drawable.ic_polygon_no_color
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_no_color
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color
Blockchain.BSC, Blockchain.BSCTestnet -> R.drawable.ic_bsc_no_color
else -> R.drawable.ic_tangem_logo
}
}
fun Blockchain.getNetworkName(): String {
return when (this) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"
Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20"
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
else -> ""
}
}

View file

@ -6,6 +6,7 @@ import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
@ -19,6 +20,7 @@ import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
import com.tangem.common.extensions.VoidCallback
fun Fragment.getDrawable(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(requireContext(), drawableResId)
}
@ -94,6 +96,13 @@ fun Context.dpToPixels(dp: Int): Int =
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun Context.pixelsToDp(pixels: Int): Int {
return (pixels.toFloat() /
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
.toInt()
}
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()

View file

@ -16,6 +16,7 @@ import com.tangem.tap.domain.extensions.setCustomIconUrl
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.network.createMoshi
import timber.log.Timber
import java.util.*
class CurrenciesRepository(val context: Application) {
@ -29,6 +30,9 @@ class CurrenciesRepository(val context: Application) {
private val obsoleteTokensAdapter: JsonAdapter<List<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, ObsoleteTokenDao::class.java)
)
private val currenciesAdapter : JsonAdapter<CurrenciesFromJson> =
moshi.adapter(CurrenciesFromJson::class.java)
fun loadCardCurrencies(cardId: String): CardCurrencies? {
val blockchains = loadSavedBlockchains(cardId).toMutableSet()
@ -116,18 +120,10 @@ class CurrenciesRepository(val context: Application) {
}
}
fun getSupportedTokens(isTestNet: Boolean = false): List<Token> {
val supportedTokens = if (isTestNet) {
getSupportedTokens().mapNotNull { it.getTestnetVersion() }
} else {
getSupportedTokens()
}
val blockchainTokens = supportedTokens.mapNotNull { blockchain ->
loadTokensJson(blockchain)?.let { fromJsonToTokensDao(it, blockchain) }
}
val tokens = blockchainTokens.flatten().map { it.toToken() }
return tokens
fun getSupportedTokens(isTestNet: Boolean = false): List<Currency> {
val fileName = if (isTestNet) "testnet_tokens" else "tokens"
val json = context.assets.readJsonFileToString(fileName)
return currenciesAdapter.fromJson(json)!!.tokens.map { Currency.fromJsonObject(it) }
}
private fun loadTokensJson(blockchain: Blockchain): String? {
@ -142,7 +138,7 @@ class CurrenciesRepository(val context: Application) {
private fun getFileName(blockchain: Blockchain): String {
return StringBuilder().apply {
append(blockchain.id.toLowerCase().replace("/test", ""))
append(blockchain.id.lowercase(Locale.getDefault()).replace("/test", ""))
append("_tokens")
appendIf("_testnet") { blockchain.isTestnet() }
}.toString()

View file

@ -0,0 +1,134 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(
val id: String,
val name: String,
val symbol: String,
val contracts: List<ContractFromJson>? = null
)
@JsonClass(generateAdapter = true)
data class ContractFromJson(
val networkId: String,
val address: String,
val decimalCount: Int
)
@JsonClass(generateAdapter = true)
data class CurrenciesFromJson(
val imageHost: String,
val tokens: List<CurrencyFromJson>
)
fun List<ContractFromJson>.toContracts(): List<Contract> {
return map { Contract.fromJsonObject(it) }
}
data class Currency(
val id: String,
val name: String,
val symbol: String,
val iconUrl: String,
val contracts: List<Contract>?
) {
companion object {
fun fromJsonObject(currency: CurrencyFromJson): Currency {
return Currency(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id),
contracts = currency.contracts?.toContracts()
)
}
}
}
data class Contract(
val networkId: String,
val blockchain: Blockchain,
val address: String,
val decimalCount: Int,
val iconUrl: String,
) {
companion object {
fun fromJsonObject(contract: ContractFromJson): Contract {
return Contract(
networkId = contract.networkId,
blockchain = Blockchain.fromNetworkId(contract.networkId),
address = contract.address,
decimalCount = contract.decimalCount,
iconUrl = getIconUrl(contract.networkId)
)
}
}
}
fun getIconUrl(id: String): String {
return "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/$id.png"
}
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain {
return when (networkId) {
"avalanche" -> Blockchain.Avalanche
"binancecoin" -> Blockchain.Binance
"binance-smart-chain" -> Blockchain.BSC
"ethereum" -> Blockchain.Ethereum
"polygon-pos" -> Blockchain.Polygon
"solana" -> Blockchain.Solana
"fantom" -> Blockchain.Fantom
"bitcoin" -> Blockchain.Bitcoin
"bitcoin-cash" -> Blockchain.BitcoinCash
"cardano" -> Blockchain.CardanoShelley
"dogecoin" -> Blockchain.Dogecoin
"ducatus" -> Blockchain.Ducatus
"litecoin" -> Blockchain.Litecoin
"rsk" -> Blockchain.RSK
"stellar" -> Blockchain.Stellar
"tezos" -> Blockchain.Tezos
"ripple" -> Blockchain.XRP
else -> Blockchain.Unknown
}
}
fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
Blockchain.Avalanche -> "avalaunch"
Blockchain.AvalancheTestnet -> "avalaunch"
Blockchain.Binance -> "binancecoin"
Blockchain.BinanceTestnet -> "binancecoin"
Blockchain.BSC -> "binance-smart-chain"
Blockchain.BSCTestnet -> "binance-smart-chain"
Blockchain.Bitcoin -> "bitcoin"
Blockchain.BitcoinTestnet -> "bitcoin"
Blockchain.BitcoinCash -> "bitcoin-cash"
Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
Blockchain.Cardano -> "cardano"
Blockchain.CardanoShelley -> "cardano"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum"
Blockchain.Fantom -> "fantom"
Blockchain.FantomTestnet -> "fantom"
Blockchain.Litecoin -> "litecoin"
Blockchain.Polygon -> "matic-network"
Blockchain.PolygonTestnet -> "matic-networks"
Blockchain.RSK -> "rsk"
Blockchain.Stellar -> "stellar"
Blockchain.StellarTestnet -> "stellar"
Blockchain.Solana -> "solana"
Blockchain.SolanaTestnet -> "solana"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "ripple"
}
}

View file

@ -7,10 +7,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -35,7 +38,12 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(getRegionProvider())) }
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(getRegionProvider())) },
onSearchTokensClick = {
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
)
}
}

View file

@ -4,6 +4,9 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@ -16,8 +19,12 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
@ -27,7 +34,8 @@ import kotlin.math.max
fun StoriesScreen(
homeState: MutableState<HomeState> = mutableStateOf(HomeState()),
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit
onShopButtonClick: () -> Unit,
onSearchTokensClick: () -> Unit,
) {
val steps = 6
val stepDuration = 8_000
@ -77,7 +85,8 @@ fun StoriesScreen(
}
)
Box(
Modifier.weight(1f)
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(isPressed) {
detectTapGestures(
@ -106,7 +115,7 @@ fun StoriesScreen(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.size(24.dp))
StoriesProgressBar(
@ -137,15 +146,44 @@ fun StoriesScreen(
6 -> StoriesWalletForEveryone()
}
}
HomeButtons(
isDarkBackground = isDarkBackground,
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp, 0.dp, 16.dp, 37.dp)
.fillMaxWidth(),
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick
)
.fillMaxWidth()
) {
if (currentStep.value == 4) Button(
onClick = onSearchTokensClick,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.height(48.dp)
,
colors = ButtonDefaults.textButtonColors(
backgroundColor = Color.White,
contentColor = Color(0xFF080C10)
)
) {
Image(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = null
)
Text(
text = stringResource(id = R.string.search_tokens_title),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}
HomeButtons(
isDarkBackground = isDarkBackground,
modifier = Modifier
.padding(start = 16.dp, top = 0.dp, end = 16.dp, bottom = 37.dp)
.fillMaxWidth(),
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick
)
}
}
}
@ -153,5 +191,5 @@ fun StoriesScreen(
@Preview
@Composable
fun InstagramScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {})
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {})
}

View file

@ -1,89 +0,0 @@
package com.tangem.tap.features.tokens.redux
import androidx.annotation.StringRes
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
sealed class CurrencyListItem {
var isAdded: Boolean = false
var isLocked: Boolean = false
data class TokenListItem(val token: Token) : CurrencyListItem()
data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem()
data class TitleListItem(
@StringRes val titleResId: Int,
var isContentShown: Boolean = true,
val blockchain: Blockchain? = null,
) : CurrencyListItem()
companion object {
fun createListOfCurrencies(
blockchains: List<Blockchain>,
tokens: List<Token>,
): List<CurrencyListItem> {
val blockchainsItemList = createBlockchainList(blockchains)
val tokenItemsLists = tokens.groupBy { it.blockchain }.map {
createTokensList(R.string.add_tokens_subtitle_format, it.key, it.value)
}
return blockchainsItemList + tokenItemsLists.flatten()
}
private fun createBlockchainList(blockchains: List<Blockchain>): List<CurrencyListItem> {
val blockchainsTitle = R.string.add_tokens_subtitle_blockchains
return listOf(TitleListItem(blockchainsTitle)) + blockchains.map { BlockchainListItem(it) }
}
private fun createTokensList(
@StringRes titleResId: Int,
blockchain: Blockchain,
tokens: List<Token>
): List<CurrencyListItem> {
val filteredTokens = tokens.filter {
it.blockchain == blockchain || it.blockchain == blockchain.getTestnetVersion()
}
val tokensListItem = filteredTokens.map { TokenListItem(it) }
return listOf(TitleListItem(titleResId, blockchain = blockchain)) + tokensListItem
}
}
}
fun List<CurrencyListItem>.removeTokensForBlockchain(blockchain: Blockchain): List<CurrencyListItem> {
return filterNot {
it is CurrencyListItem.TokenListItem && it.isTheSameOrTestnetBlockchain(blockchain)
}
}
fun List<CurrencyListItem>.addTokensForBlockchain(
blockchain: Blockchain, fullCurrenciesList: List<CurrencyListItem>,
): List<CurrencyListItem> {
val tokensToAdd = fullCurrenciesList.filter {
it is CurrencyListItem.TokenListItem && it.isTheSameOrTestnetBlockchain(blockchain)
}
val indexToInsert = indexOfFirst {
it is CurrencyListItem.TitleListItem && it.isTheSameOrTestnetBlockchain(blockchain)
} + 1
return this.toMutableList().apply { addAll(indexToInsert, tokensToAdd) }
}
fun List<CurrencyListItem>.toggleHeaderContentShownValue(blockchain: Blockchain) {
map {
if (it is CurrencyListItem.TitleListItem && it.isTheSameOrTestnetBlockchain(blockchain)) {
it.isContentShown = !it.isContentShown
}
}
}
// if you do not check the testNet blockchains, it will not collapse
private fun CurrencyListItem.isTheSameOrTestnetBlockchain(blockchain: Blockchain): Boolean {
return when (this) {
is CurrencyListItem.TitleListItem -> this.blockchain?.isTheSameOrTestnetBlockchain(blockchain) ?: false
is CurrencyListItem.BlockchainListItem -> this.blockchain.isTheSameOrTestnetBlockchain(blockchain)
is CurrencyListItem.TokenListItem -> this.token.blockchain.isTheSameOrTestnetBlockchain(blockchain)
}
}
private fun Blockchain.isTheSameOrTestnetBlockchain(blockchain: Blockchain): Boolean {
return this == blockchain || this == blockchain.getTestnetVersion()
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.Action
@ -8,14 +10,17 @@ sealed class TokensAction : Action {
object ResetState : TokensAction()
data class AllowToAddTokens(val allow: Boolean) : TokensAction()
object LoadCurrencies : TokensAction() {
data class Success(val currencies: List<CurrencyListItem>) : TokensAction()
data class Success(val currencies: List<Currency>) : TokensAction()
}
data class SetAddedCurrencies(val wallets: List<WalletData>) : TokensAction()
data class SetNonRemovableCurrencies(val wallets: List<WalletData>) : TokensAction()
data class ToggleShowTokensForBlockchain(val isShown: Boolean, val blockchain: Blockchain) : TokensAction()
object OpenAllTokens : TokensAction()
data class SaveChanges(val addedItems: List<CurrencyListItem>) : TokensAction()
data class SaveChanges(
val addedTokens: List<Token>,
val addedBlockchains: List<Blockchain>
) : TokensAction()
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
@ -33,6 +34,11 @@ class TokensMiddleware {
{ next ->
{ action ->
when (action) {
is NavigationAction.NavigateTo -> {
if (action.screen == AppScreen.AddTokens) {
store.dispatch(TokensAction.LoadCurrencies)
}
}
is TokensAction.LoadCurrencies -> handleLoadCurrencies()
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
@ -42,34 +48,37 @@ class TokensMiddleware {
}
private fun handleLoadCurrencies() {
val scanResponse = store.state.globalState.scanResponse ?: return
val isTestcard = scanResponse.card.isTestCard
val scanResponse = store.state.globalState.scanResponse
val isTestcard = scanResponse?.card?.isTestCard ?: false
val currencies = currenciesRepository.getSupportedTokens(isTestcard)
val tokens = currenciesRepository.getSupportedTokens(isTestcard)
val blockchains = currenciesRepository.getBlockchains(
cardFirmware = scanResponse.card.firmwareVersion,
isTestNet = isTestcard
).sortedBy { it.fullName }
val currencies = CurrencyListItem.createListOfCurrencies(blockchains, tokens)
store.dispatch(TokensAction.LoadCurrencies.Success(currencies))
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
val scanResponse = store.state.globalState.scanResponse ?: return
val candidatesToAdd = action.addedItems
if (candidatesToAdd.isEmpty()) return
val currentTokens = store.state.tokensState.addedWallets.toTokens()
val currentBlockchains = store.state.tokensState.addedWallets.toBlockchains()
val blockchains = candidatesToAdd.filterIsInstance<CurrencyListItem.BlockchainListItem>()
.map { it.blockchain }
val tokens = candidatesToAdd.filterIsInstance<CurrencyListItem.TokenListItem>()
.map { it.token }
if (blockchains.isEmpty() && tokens.isEmpty()) return
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
val blockchainsToRemove = currentBlockchains.filter { !action.addedBlockchains.contains(it) }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
val tokensToRemove = currentTokens.filter { !action.addedTokens.contains(it) }
removeCurrenciesIfNeeded(blockchainsToRemove, tokensToRemove)
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
store.dispatch(NavigationAction.PopBackTo())
return
}
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, blockchains, tokens)
deriveMissingBlockchains(scanResponse, blockchainsToAdd, tokensToAdd)
} else {
submitAdd(blockchains, tokens)
submitAdd(blockchainsToAdd, tokensToAdd)
store.dispatch(NavigationAction.PopBackTo())
}
}
@ -158,4 +167,22 @@ class TokensMiddleware {
WalletAction.MultiWallet.AddToken(it)
}).forEach { store.dispatchOnMain(it) }
}
private fun removeCurrenciesIfNeeded(blockchains: List<Blockchain>, tokens: List<Token>) {
if (tokens.isNotEmpty()) {
tokens.forEach { token ->
store.state.walletState.getWalletData(token)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
}
}
}
if (blockchains.isNotEmpty()) {
blockchains.forEach { blockchain ->
store.state.walletState.getWalletData(blockchain)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
}
}
}
}
}

View file

@ -1,9 +1,6 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.Action
class TokensReducer {
@ -19,35 +16,25 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
return when (action) {
is TokensAction.ResetState -> TokensState()
is TokensAction.LoadCurrencies.Success -> {
tokensState.copy(currencies = action.currencies, shownCurrencies = action.currencies)
tokensState.copy(currencies = action.currencies)
}
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(addedCurrencies = action.wallets.toCardCurrencies())
tokensState.copy(
addedBlockchains = action.wallets.toBlockchains(),
addedTokens = action.wallets.toTokens(),
addedWallets = action.wallets,
)
}
is TokensAction.ToggleShowTokensForBlockchain -> {
if (action.isShown) {
val shownCurrencies = tokensState.shownCurrencies
.removeTokensForBlockchain(action.blockchain)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
tokensState.copy(shownCurrencies = shownCurrencies)
} else {
val shownCurrencies = tokensState.shownCurrencies
.addTokensForBlockchain(action.blockchain, tokensState.currencies)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
tokensState.copy(shownCurrencies = shownCurrencies)
}
is TokensAction.SetNonRemovableCurrencies -> {
tokensState.copy(
nonRemovableBlockchains = action.wallets.toBlockchains(),
nonRemovableTokens = action.wallets.toTokensContractAddresses(),
)
}
is TokensAction.OpenAllTokens -> {
val shownCurrencies = tokensState.currencies
shownCurrencies.forEach { if (it is CurrencyListItem.TitleListItem) it.isContentShown = true}
tokensState.copy(shownCurrencies = tokensState.currencies)
is TokensAction.AllowToAddTokens -> {
tokensState.copy(allowToAdd = action.allow)
}
else -> tokensState
}
}
private fun List<WalletData>.toCardCurrencies(): CardCurrencies {
val tokens = mapNotNull { (it.currency as? Currency.Token)?.token }.distinct()
val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain }.distinct()
return CardCurrencies(tokens = tokens, blockchains = blockchains)
}

View file

@ -1,18 +1,31 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.StateType
data class TokensState(
val addedTokens: LinkedHashSet<TokenWithAmount> = LinkedHashSet(),
val addedCurrencies: CardCurrencies? = null,
val currencies: List<CurrencyListItem> = emptyList(),
val shownCurrencies: List<CurrencyListItem> = emptyList(),
val addedWallets: List<WalletData> = emptyList(),
val addedTokens: List<Token> = emptyList(),
val addedBlockchains: List<Blockchain> = emptyList(),
val nonRemovableTokens: List<ContractAddress> = emptyList(),
val nonRemovableBlockchains: List<Blockchain> = emptyList(),
val currencies: List<Currency> = emptyList(),
val allowToAdd: Boolean = true
) : StateType
data class TokenWithAmount(
val token: Token,
val amount: Amount? = null
)
typealias ContractAddress = String
fun List<WalletData>.toTokensContractAddresses(): List<ContractAddress> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token?.contractAddress }.distinct()
}
fun List<WalletData>.toTokens(): List<Token> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }.distinct()
}
fun List<WalletData>.toBlockchains(): List<Blockchain> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Blockchain)?.blockchain }.distinct()
}

View file

@ -8,15 +8,17 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter
import com.tangem.tap.features.tokens.ui.compose.CurrenciesScreen
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
@ -27,10 +29,11 @@ import org.rekotlin.StoreSubscriber
class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
StoreSubscriber<TokensState> {
StoreSubscriber<TokensState> {
private lateinit var viewAdapter: CurrenciesAdapter
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
private var searchInput = mutableStateOf("")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -64,27 +67,24 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupPopularTokensRecyclerView()
btnTokensSaveChanges.setOnClickListener {
store.dispatch(TokensAction.SaveChanges(viewAdapter.getAddedItems()))
}
}
private fun setupPopularTokensRecyclerView() = with(binding) {
viewAdapter = CurrenciesAdapter()
viewAdapter.setOnItemAddListener {
btnTokensSaveChanges.isEnabled = viewAdapter.getAddedItems().isNotEmpty()
cvCurrencies.setContent {
AppCompatTheme {
CurrenciesScreen(
tokensState = tokensState,
searchInput = searchInput
) { tokens, blockchains ->
store.dispatch(TokensAction.SaveChanges(tokens, blockchains))
}
}
}
rvPopularTokens.layoutManager = LinearLayoutManager(context)
rvPopularTokens.adapter = viewAdapter
}
override fun newState(state: TokensState) {
if (activity == null || view == null) return
viewAdapter.addedCurrencies = state.addedCurrencies
viewAdapter.submitUnfilteredList(state.shownCurrencies)
val toolbarTitle =
if (state.allowToAdd) R.string.add_tokens_title else R.string.search_tokens_title
binding.toolbar.title = getString(toolbarTitle)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -102,13 +102,13 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
searchView.queryHint = searchView.getString(R.string.add_token_search_hint)
searchView.maxWidth = android.R.attr.width
searchView.inputtedTextAsFlow()
.debounce(400)
.distinctUntilChanged()
.onEach {
viewAdapter.filter(searchView.query)
}
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater);
.debounce(400)
.distinctUntilChanged()
.onEach {
searchInput.value = it.lowercase()
}
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater)
}
override fun onDestroy() {

View file

@ -1,254 +0,0 @@
package com.tangem.tap.features.tokens.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.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.tokens.getTokensName
import com.tangem.tap.features.tokens.redux.CurrencyListItem
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemCurrencySubtitleBinding
import com.tangem.wallet.databinding.ItemPopularTokenBinding
import java.util.*
class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>(DiffUtilCallback) {
var addedCurrencies: CardCurrencies? = null
private var unfilteredList = listOf<CurrencyListItem>()
private val currentlyAddedItems = mutableListOf<CurrencyListItem>()
private var vhOnItemAddListener: ((CurrencyListItem) -> Unit) = {
currentlyAddedItems.add(it)
onItemAddListener?.invoke()
}
private var onItemAddListener: VoidCallback? = null
fun submitUnfilteredList(list: List<CurrencyListItem>) {
unfilteredList = list
submitList(list)
}
fun setOnItemAddListener(itemAddListener: VoidCallback) {
onItemAddListener = itemAddListener
if (currentlyAddedItems.isNotEmpty()) itemAddListener()
}
fun getAddedItems(): List<CurrencyListItem> {
return currentlyAddedItems.toList()
}
override fun getItemViewType(position: Int): Int {
return when (currentList[position]) {
is CurrencyListItem.TitleListItem -> 0
is CurrencyListItem.BlockchainListItem, is CurrencyListItem.TokenListItem -> 1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
0 -> TitleViewHolder(
binding = ItemCurrencySubtitleBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
1 -> CurrenciesViewHolder(
binding = ItemPopularTokenBinding
.inflate(LayoutInflater.from(parent.context), parent, false),
onItemAddListener = vhOnItemAddListener
)
else -> CurrenciesViewHolder(
binding = ItemPopularTokenBinding
.inflate(LayoutInflater.from(parent.context), parent, false),
onItemAddListener = vhOnItemAddListener
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val listItem = currentList[position]
if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) {
holder.bind(listItem)
} else if (holder is CurrenciesViewHolder) {
holder.bind(listItem, addedCurrencies, currentlyAddedItems)
}
}
object DiffUtilCallback : DiffUtil.ItemCallback<CurrencyListItem>() {
override fun areContentsTheSame(
oldItem: CurrencyListItem, newItem: CurrencyListItem,
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: CurrencyListItem, newItem: CurrencyListItem,
) = oldItem == newItem
}
fun filter(query: CharSequence?) {
val list = mutableListOf<CurrencyListItem>()
if (!query.isNullOrEmpty()) {
store.dispatch(TokensAction.OpenAllTokens)
val queryNormalized = query.toString().toLowerCase(Locale.US)
list.addAll(
unfilteredList.filter { element ->
when (element) {
is CurrencyListItem.BlockchainListItem -> {
element.blockchain.currency.toLowerCase(Locale.US)
.contains(queryNormalized) ||
element.blockchain.fullName.toLowerCase(Locale.US)
.contains(queryNormalized)
}
is CurrencyListItem.TokenListItem -> {
element.token.name.toLowerCase(Locale.US)
.contains(queryNormalized) ||
element.token.symbol.toLowerCase(Locale.US)
.contains(queryNormalized)
}
is CurrencyListItem.TitleListItem -> true
}
})
} else {
list.addAll(unfilteredList)
}
submitList(list)
}
class CurrenciesViewHolder(
private val binding: ItemPopularTokenBinding,
private val onItemAddListener: ((CurrencyListItem) -> Unit),
) : RecyclerView.ViewHolder(binding.root) {
fun bind(
currency: CurrencyListItem,
addedCurrencies: CardCurrencies?,
currentlyAddedItems: MutableList<CurrencyListItem>
) {
when (currency) {
is CurrencyListItem.BlockchainListItem -> {
val addedBlockchains = addedCurrencies?.blockchains ?: listOf()
val currentlyAdded =
currentlyAddedItems.filterIsInstance<CurrencyListItem.BlockchainListItem>()
bindBlockchain(currency, addedBlockchains, currentlyAdded)
}
is CurrencyListItem.TokenListItem -> {
val addedTokens = addedCurrencies?.tokens ?: listOf()
val currentlyAdded =
currentlyAddedItems.filterIsInstance<CurrencyListItem.TokenListItem>()
bindToken(currency, addedTokens, currentlyAdded)
}
}
}
private fun bindBlockchain(
currency: CurrencyListItem.BlockchainListItem,
addedBlockchains: List<Blockchain>,
currentlyAdded: List<CurrencyListItem.BlockchainListItem>
) = with(binding) {
val blockchain = currency.blockchain
Picasso.get().loadCurrenciesIcon(ivCurrency, tvTokenLetter, null, blockchain)
tvCurrencyName.text = blockchain.fullName
tvCurrencySymbol.text = blockchain.currency
btnAddToken.setOnClickListener {
onItemAddListener.invoke(currency)
currency.isAdded = true
modifyAddTokenButton(currency)
}
val isAddedBefore = addedBlockchains.contains(blockchain)
val isCurrentlyAdded = currentlyAdded.any { it.blockchain == blockchain }
currency.isAdded = isAddedBefore || isCurrentlyAdded
modifyAddTokenButton(currency)
}
private fun bindToken(
currency: CurrencyListItem.TokenListItem,
addedTokens: List<Token>,
currentlyAdded: List<CurrencyListItem.TokenListItem>
) = with(binding) {
val token = currency.token
Picasso.get()
.loadCurrenciesIcon(ivCurrency, tvTokenLetter, token, token.blockchain)
tvCurrencyName.text = token.name
tvCurrencySymbol.text = tvCurrencySymbol.getString(
R.string.token_symbol_address_format,
token.symbol, token.contractAddress
)
btnAddToken.setOnClickListener {
onItemAddListener.invoke(currency)
currency.isAdded = true
modifyAddTokenButton(currency)
}
val isAddedBefore = addedTokens.any { it == token }
val isCurrentlyAdded = currentlyAdded.any { it.token == token }
currency.isAdded = isAddedBefore || isCurrentlyAdded
modifyAddTokenButton(currency)
}
private fun modifyAddTokenButton(currency: CurrencyListItem) = with(binding) {
if (currency.isLocked) {
btnAddToken.setText(R.string.common_add)
btnAddToken.isEnabled = false
} else {
val text = if (currency.isAdded) R.string.add_token_added else R.string.common_add
btnAddToken.setText(text)
btnAddToken.isEnabled = !currency.isAdded
}
}
}
class TitleViewHolder(val binding: ItemCurrencySubtitleBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(title: CurrencyListItem.TitleListItem) = with(binding) {
if (title.blockchain != null) {
val subtitle = root.getString(title.titleResId, title.blockchain.getTokensName())
tvSubtitle.text = subtitle.uppercase()
clSubtitleContainer.setOnClickListener {
val rotation = if (title.isContentShown) -90f else 90f
store.dispatch(
TokensAction.ToggleShowTokensForBlockchain(
title.isContentShown, title.blockchain
)
)
ivToggleSublistVisibility.animate().rotation(rotation).withEndAction {
showArrow(title.isContentShown)
}
}
showArrow(title.isContentShown)
} else {
tvSubtitle.text = root.getString(title.titleResId).uppercase()
ivToggleSublistVisibility.hide()
}
}
private fun showArrow(isContentShown: Boolean) = with(binding) {
ivToggleSublistVisibility.show()
val drawable = if (isContentShown) {
R.drawable.ic_arrow_angle_down
} else {
R.drawable.ic_arrow_angle_right
}
ivToggleSublistVisibility.setImageResource(drawable)
ivToggleSublistVisibility.rotation = 0f
}
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getRoundIconRes
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.wallet.R
@Composable
fun CollapsedCurrencyItem(
currency: Currency,
addedTokens: List<Token>,
addedBlockchains: List<Blockchain>,
onCurrencyClick: (String) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { onCurrencyClick(currency.id) })
) {
val blockchain = Blockchain.fromNetworkId(currency.id)
val iconRes = if (blockchain == Blockchain.Unknown) {
currency.iconUrl
} else {
blockchain.getRoundIconRes()
}
SubcomposeAsyncImage(
model = iconRes,
contentDescription = currency.id,
loading = { CurrencyPlaceholderIcon(currency.id) },
error = { CurrencyPlaceholderIcon(currency.id) },
modifier = Modifier
.clip(CircleShape)
.padding(16.dp)
.size(46.dp),
)
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.align(Alignment.CenterVertically)
) {
Text(
text = currency.name,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),
)
Spacer(modifier = Modifier.size(6.dp))
Row {
if (currency.contracts != null) {
currency.contracts.map { contract ->
val added =
addedTokens.map { it.contractAddress }.contains(contract.address)
val icon = if (added) {
contract.blockchain.getRoundIconRes()
} else {
contract.blockchain.getGreyedOutIconRes()
}
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
Spacer(modifier = Modifier.size(5.dp))
}
} else {
val added = addedBlockchains.contains(blockchain)
val icon =
if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes()
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
}
}
}
Image(
painter = painterResource(id = R.drawable.ic_arrow_collapsed),
contentDescription = null,
Modifier
.padding(20.dp)
.align(Alignment.CenterVertically)
)
}
}

View file

@ -0,0 +1,101 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.extensions.pixelsToDp
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.store
import com.tangem.wallet.R
@Composable
fun CurrenciesScreen(
tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState),
searchInput: MutableState<String>,
onSaveChanges: (List<Token>, List<Blockchain>) -> Unit
) {
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
val isKeyboardOpen by keyboardAsState()
val onAddCurrencyToggleClick = { currency: Currency, contract: Token? ->
if (contract != null) {
val mutableList = addedTokensState.value.toMutableList()
if (mutableList.contains(contract)) {
mutableList.remove(contract)
} else {
mutableList.add(contract)
}
addedTokensState.value = mutableList
} else {
val blockchain = Blockchain.fromNetworkId(currency.id)
val mutableList = addedBlockchainsState.value.toMutableList()
if (mutableList.contains(blockchain)) {
mutableList.remove(blockchain)
} else {
mutableList.add(blockchain)
}
addedBlockchainsState.value = mutableList
}
}
Scaffold(
// topBar =,
floatingActionButton = {
if (tokensState.value.allowToAdd) SaveChangesButton(isKeyboardOpen) {
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
}
},
floatingActionButtonPosition = FabPosition.Center,
) {
ListOfCurrencies(
currencies = tokensState.value.currencies,
nonRemovableTokens = tokensState.value.nonRemovableTokens,
nonRemovableBlockchains = tokensState.value.nonRemovableBlockchains,
addedTokens = addedTokensState.value,
addedBlockchains = addedBlockchainsState.value,
searchInput = searchInput.value,
allowToAdd = tokensState.value.allowToAdd,
onAddCurrencyToggled = onAddCurrencyToggleClick
)
}
}
@Composable
fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
val padding = if (keyboardState is Keyboard.Opened) {
LocalContext.current.pixelsToDp(keyboardState.height)
} else {
0
}
ExtendedFloatingActionButton(
text = {
Text(
text = stringResource(id = R.string.common_save_changes),
)
},
onClick = onSaveChanges,
backgroundColor = Color(0xFF1ACE80),
contentColor = Color.White,
modifier = Modifier.padding(bottom = padding.dp)
)
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.runtime.Composable
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
@Composable
fun CurrencyItem(
currency: Currency,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<Token>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
expanded: Boolean,
onCurrencyClick: (String) -> Unit,
onAddCurrencyToggled: (Currency, Token?) -> Unit
) {
if (expanded) {
ExpandedCurrencyItem(
currency = currency,
nonRemovableTokens = nonRemovableTokens,
nonRemovableBlockchains = nonRemovableBlockchains,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,
onCurrencyClick = onCurrencyClick, onAddCurrencyToggled = onAddCurrencyToggled
)
} else {
CollapsedCurrencyItem(
currency = currency, addedTokens = addedTokens,
addedBlockchains = addedBlockchains, onCurrencyClick
)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun CurrencyPlaceholderIcon(id: String) {
val letterColor: Color = Color.White
val circleColor: Color = Color.Black
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(circleColor, shape = CircleShape)
)
{
Text(
text = id.firstOrNull()?.titlecase() ?: "",
textAlign = TextAlign.Center,
color = letterColor,
modifier = Modifier.padding(4.dp),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
)
}
}

View file

@ -0,0 +1,155 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.getRoundIconRes
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.wallet.R
@Composable
fun ExpandedCurrencyItem(
currency: Currency,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<Token>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
onCurrencyClick: (String) -> Unit,
onAddCurrencyToggled: (Currency, Token?) -> Unit
) {
val blockchain = Blockchain.fromNetworkId(currency.id)
val iconRes = if (blockchain == Blockchain.Unknown) {
currency.iconUrl
} else {
blockchain.getRoundIconRes()
}
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { onCurrencyClick(currency.id) })
) {
SubcomposeAsyncImage(
model = iconRes,
contentDescription = currency.id,
loading = { CurrencyPlaceholderIcon(currency.id) },
error = { CurrencyPlaceholderIcon(currency.id) },
modifier = Modifier
.clip(CircleShape)
.padding(start = 16.dp, top = 16.dp, end = 16.dp)
.size(46.dp),
)
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.padding(top = 14.dp)
.align(Alignment.CenterVertically)
) {
Text(
text = currency.name,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),
)
Spacer(modifier = Modifier.size(6.dp))
Text(
text = stringResource(id = R.string.currency_subtitle_expanded),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF8E8E93),
)
}
Image(
painter = painterResource(id = R.drawable.ic_arrow_extended),
contentDescription = null,
Modifier
.padding(20.dp)
.align(Alignment.CenterVertically)
)
}
val blockchains = currency.contracts?.map { it.blockchain } ?: listOf(
Blockchain.fromNetworkId(currency.id)
)
Row {
Box(
modifier = Modifier
.fillMaxHeight()
) {
if (blockchains.size > 1) {
Divider(
color = Color(0xFFDEDEDE),
modifier = Modifier
.height((43 * blockchains.size - 1).dp)
.padding(start = 37.dp, top = 16.dp)
.width(1.dp)
)
}
Column {
blockchains.map {
Image(
painter = painterResource(id = R.drawable.ic_link),
contentDescription = null,
Modifier
.padding(
start = 37.dp,
)
)
Spacer(modifier = Modifier.size(13.5.dp))
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 6.dp),
) {
blockchains.map { blockchain ->
val contract = currency.contracts?.firstOrNull { it.blockchain == blockchain }
val added = if (contract != null) {
addedTokens.map { it.contractAddress }.contains(contract.address)
} else {
addedBlockchains.contains(blockchain)
}
val canBeRemoved = if (contract != null) {
!nonRemovableTokens.contains(contract.address)
} else {
!nonRemovableBlockchains.contains(blockchain)
}
NetworkItem(
currency = currency,
contract = contract,
blockchain = blockchain, allowToAdd = allowToAdd,
added = added,
canBeRemoved = canBeRemoved,
onAddCurrencyToggled = onAddCurrencyToggled
)
}
}
}
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
@Composable
fun ListOfCurrencies(
currencies: List<Currency>,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<Token>,
addedBlockchains: List<Blockchain>,
searchInput: String,
allowToAdd: Boolean,
onAddCurrencyToggled: (Currency, Token?) -> Unit
) {
val expandedCurrencies = remember { mutableStateOf(listOf("")) }
val onCurrencyClick = { currencyId: String ->
val mutableList = expandedCurrencies.value.toMutableList()
if (mutableList.contains(currencyId)) {
mutableList.remove(currencyId)
} else {
mutableList.add(currencyId)
}
expandedCurrencies.value = mutableList
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
val filteredCurrencies = if (searchInput.isBlank()) {
currencies
} else {
currencies.asSequence().filter {
it.name.lowercase().contains(searchInput) || it.symbol.lowercase()
.contains(searchInput)
}.toList()
}
items(filteredCurrencies) { currency ->
CurrencyItem(
currency = currency,
nonRemovableTokens = nonRemovableTokens,
nonRemovableBlockchains = nonRemovableBlockchains,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,
expanded = expandedCurrencies.value.contains(currency.id),
onCurrencyClick = onCurrencyClick,
onAddCurrencyToggled = onAddCurrencyToggled
)
}
}
}

View file

@ -0,0 +1,85 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.common.extensions.getRoundIconRes
import com.tangem.tap.domain.tokens.Contract
import com.tangem.tap.domain.tokens.Currency
@Composable
fun NetworkItem(
currency: Currency, contract: Contract?,
blockchain: Blockchain, allowToAdd: Boolean,
added: Boolean, canBeRemoved: Boolean,
onAddCurrencyToggled: (Currency, Token?) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
SubcomposeAsyncImage(
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
contentDescription = blockchain.fullName,
loading = { CurrencyPlaceholderIcon(blockchain.id) },
error = { CurrencyPlaceholderIcon(blockchain.id) },
modifier = Modifier
.clip(CircleShape)
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
.size(20.dp)
.align(Alignment.CenterVertically)
)
Row(modifier = Modifier
.fillMaxHeight()
.weight(1f)
.align(Alignment.CenterVertically)) {
Text(
text = blockchain.name.uppercase(),
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
color = if (added) Color.Black else Color(0xFF848488),
)
Spacer(modifier = Modifier.size(3.dp))
Text(
text = if (contract != null) blockchain.getNetworkName().uppercase() else "MAIN",
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (contract != null) Color(0xFF8E8E93) else Color(0xFF1ACE80),
)
}
val token = if (contract != null) {
Token(
name = currency.name, symbol = currency.symbol, contractAddress = contract.address,
decimals = contract.decimalCount, blockchain = contract.blockchain
)
} else {
null
}
if (allowToAdd) Switch(
checked = added,
enabled = canBeRemoved,
onCheckedChange = { onAddCurrencyToggled(currency, token) },
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFF1ACE80)
)
)
}
}

View file

@ -98,7 +98,11 @@ class MultiWalletView : WalletView {
binding.btnAddToken.setOnClickListener {
store.dispatch(TokensAction.LoadCurrencies)
store.dispatch(TokensAction.AllowToAddTokens(true))
store.dispatch(TokensAction.SetAddedCurrencies(state.wallets))
store.dispatch(TokensAction.SetNonRemovableCurrencies(
state.wallets.filterNot { state.canBeRemoved(it) })
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)

View file

@ -23,7 +23,9 @@ class OnramperService(
) : ExchangeService, ExchangeUrlBuilder {
private val api: OnramperApi by lazy {
createRetrofitInstance(OnramperApi.BASE_URL, listOf(AddKeyToHeaderInterceptor(apiKey)))
createRetrofitInstance(OnramperApi.BASE_URL,
listOf(AddKeyToHeaderInterceptor(apiKey))
)
.create(OnramperApi::class.java)
}