Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-13 19:43:03 +03:00
commit 12bc959fde
112 changed files with 3193 additions and 47273 deletions

View file

@ -82,10 +82,9 @@ 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-66'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-140'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-140'
implementation 'com.tangem:blockchain:develop-70'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
// WebView
implementation "androidx.browser:browser:1.3.0"
@ -128,7 +127,11 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.12.0'
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")
//
// //Crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"
@ -147,10 +150,16 @@ dependencies {
//Compose
implementation 'androidx.activity:activity-compose:1.4.0'
implementation "androidx.compose.material:material:1.1.1"
implementation 'androidx.compose.material:material:1.1.1'
implementation 'androidx.compose.animation:animation:1.1.1'
implementation 'androidx.compose.foundation:foundation:1.1.1'
implementation 'androidx.compose.foundation:foundation-layout:1.1.1'
implementation 'androidx.compose.ui:ui:1.1.1'
implementation 'androidx.compose.ui:ui-tooling:1.1.1'
implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.1"
implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0"
implementation("io.coil-kt:coil:2.0.0-rc01")
implementation("io.coil-kt:coil-compose:2.0.0-rc01")
implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'

File diff suppressed because it is too large Load diff

View file

@ -1,16 +0,0 @@
[
{
"decimalCount": 6,
"customIconUrl": "https://i.imgur.com\/LPZDXoT.png",
"symbol": "FUJISTABLE",
"name": "The Fuji stablecoin",
"contractAddress": "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22"
},
{
"decimalCount": 9,
"customIconUrl": "https://i.imgur.com\/jNTqyQA.png",
"symbol": "FUJIMOON",
"name": "To the Moon",
"contractAddress": "0x97132C109c6816525F7f338DCb7435E1412A7668"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,9 +0,0 @@
[
{
"decimalCount": 8,
"customIconUrl": "https:\/\/assets.trustwalletapp.com\/blockchains\/binance\/assets\/MATIC-84A\/logo.png",
"symbol": "HEM",
"name": "Hemster - 452",
"contractAddress": "HEM-452"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,65 +0,0 @@
[
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c/logo.png",
"symbol": "BTCB",
"name": "Binance-Peg BTCB Token",
"contractAddress": "0x6ce8da28e2f864420840cf74474eff5fd80e65b8"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56/logo.png",
"symbol": "BUSD",
"name": "Binance-Peg BUSD Token",
"contractAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3/logo.png",
"symbol": "DAI",
"name": "Binance-Peg Dai Token",
"contractAddress": "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x55d398326f99059fF775485246999027B3197955/logo.png",
"symbol": "USDT",
"name": "Binance-Peg Tether USD",
"contractAddress": "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd"
},
{
"decimalCount": 18,
"customIconUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
"symbol": "USDC",
"name": "Binance-Peg USDC Token",
"contractAddress": "0x64544969ed7ebf5f083679233325356ebe738930"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1D2F0da169ceB9fC7B3144628dB156f3F6c60dBE/logo.png",
"symbol": "XRP",
"name": "Binance-Peg XRP",
"contractAddress": "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3/logo.png",
"symbol": "DAI",
"name": "Dai Token",
"contractAddress": "0x8a9424745056eb399fd19a0ec26a14316684e274"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x2170Ed0880ac9A755fd29B2688956BD959F933F8/logo.png",
"symbol": "ETH",
"name": "Ethereum Token",
"contractAddress": "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378"
},
{
"decimalCount": 18,
"customIconUrl": "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x55d398326f99059fF775485246999027B3197955/logo.png",
"symbol": "USDT",
"name": "Tether USD",
"contractAddress": "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,26 +0,0 @@
[
{
"decimalCount": 18,
"symbol": "WEENUS",
"name": "Weenus",
"contractAddress": "0xaFF4481D10270F50f203E0763e2597776068CBc5"
},
{
"decimalCount": 18,
"symbol": "XEENUS",
"name": "Xeenus",
"contractAddress": "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c"
},
{
"decimalCount": 8,
"symbol": "YEENUS",
"name": "Yeenus",
"contractAddress": "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7"
},
{
"decimalCount": 0,
"symbol": "ZEENUS",
"name": "Zeenus",
"contractAddress": "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,16 +0,0 @@
[
{
"decimalCount": 18,
"customIconUrl": "https://cdn.worldvectorlogo.com/logos/usd-1.svg",
"symbol": "FUSD",
"name": "Fantom USD",
"contractAddress": "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074"
},
{
"decimalCount": 18,
"customIconUrl": "https://cryptologos.cc/logos/fantom-ftm-logo.svg?v=003",
"symbol": "WFTM",
"name": "Wrapped Fantom",
"contractAddress": "0xf1277d1Ed8AD466beddF92ef448A132661956621"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,14 +0,0 @@
[
{
"decimalCount": 9,
"symbol": "TCA",
"name": "Tangem Coin A",
"contractAddress": "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR"
},
{
"decimalCount": 9,
"symbol": "TCB",
"name": "Tangem Coin B",
"contractAddress": "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4"
}
]

View file

@ -0,0 +1,369 @@
{
"tokens" : [
{
"id" : "polygon/test",
"symbol" : "MATIC",
"name" : "Polygon",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
}
]
},
{
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount" : 18
}
]
},
{
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
}
]
},
{
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
}
]
},
{
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
}
]
},
{
"id" : "Tether USD-USDT",
"symbol" : "USDT",
"name" : "Tether USD",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
},
{
"networkId" : "binance-smart-chain",
"address" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"contracts" : [
{
"networkId" : "polygon-pos",
"address" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
}
]
},
{
"id" : "solana/test",
"symbol" : "SOL",
"name" : "Solana",
"contracts" : [
]
},
{
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"contracts" : [
{
"networkId" : "solana",
"address" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
}
]
},
{
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"contracts" : [
{
"networkId" : "solana",
"address" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
}
]
},
{
"id" : "binance/test",
"symbol" : "BNB",
"name" : "Binance",
"contracts" : [
]
},
{
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"contracts" : [
{
"networkId" : "binancecoin",
"address" : "HEM-452",
"decimalCount" : 8
}
]
},
{
"id" : "bsc/test",
"symbol" : "BNB",
"name" : "Bsc",
"contracts" : [
]
},
{
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"address" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
}
]
},
{
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"address" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
}
]
},
{
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"address" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
}
]
},
{
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"address" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
}
]
},
{
"id" : "ethereum/test",
"symbol" : "ETH",
"name" : "Ethereum",
"contracts" : [
]
},
{
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"contracts" : [
{
"networkId" : "ethereum",
"address" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
}
]
},
{
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"contracts" : [
{
"networkId" : "ethereum",
"address" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
}
]
},
{
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"contracts" : [
{
"networkId" : "ethereum",
"address" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
}
]
},
{
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"contracts" : [
{
"networkId" : "ethereum",
"address" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
}
]
},
{
"id" : "avalanche/test",
"symbol" : "AVAX",
"name" : "Avalanche",
"contracts" : [
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"contracts" : [
{
"networkId" : "avalanche",
"address" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
}
]
},
{
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"contracts" : [
{
"networkId" : "avalanche",
"address" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
}
]
},
{
"id" : "fantom/test",
"symbol" : "FTM",
"name" : "Fantom",
"contracts" : [
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"contracts" : [
{
"networkId" : "fantom",
"address" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"contracts" : [
{
"networkId" : "fantom",
"address" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
}
]
},
{
"id" : "bitcoin/test",
"symbol" : "BTC",
"name" : "Bitcoin",
"contracts" : [
]
},
{
"id" : "stellar/test",
"symbol" : "XLM",
"name" : "Stellar",
"contracts" : [
]
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -106,6 +106,7 @@ object TangemSdk {
is TangemSdkError.IssuerSignatureLoadingFailed -> TangemSdkError.IssuerSignatureLoadingFailed()
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch()
}
}

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

@ -5,20 +5,54 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.wallet.R
@DrawableRes
fun Blockchain.getIconRes(): Int {
fun Blockchain.getRoundIconRes(): Int {
return when (this) {
Blockchain.Unknown -> R.drawable.shape_circle
Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet,-> R.drawable.ic_btc
Blockchain.BitcoinCash -> R.drawable.ic_btc_cash
Blockchain.Litecoin -> R.drawable.ic_ltc
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth
Blockchain.RSK -> R.drawable.ic_rsk
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano
Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_binance
Blockchain.Tezos -> R.drawable.ic_tezos
Blockchain.XRP -> R.drawable.ic_xrp
Blockchain.Stellar -> R.drawable.ic_stellar
else -> R.drawable.shape_circle
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.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, Blockchain.Binance, Blockchain.BinanceTestnet -> 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.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, Blockchain.Binance, Blockchain.BinanceTestnet -> 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

@ -1,7 +1,6 @@
package com.tangem.tap.common.extensions
import android.graphics.*
import android.net.Uri
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import com.squareup.picasso.Callback
@ -11,6 +10,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.domain.tokens.toNetworkId
import com.tangem.wallet.R
fun Picasso.loadCurrenciesIcon(
@ -20,10 +21,11 @@ fun Picasso.loadCurrenciesIcon(
blockchain: Blockchain,
) {
val url = if (token != null) {
token.getCustomIconUrl()?.let { Uri.parse(it) } ?: IconsUtil.getTokenIconUri(blockchain, token)
val url: String? = if (token != null) {
token.id?.let { getIconUrl(it) } ?: token.getCustomIconUrl()
?: IconsUtil.getTokenIconUri(blockchain, token)?.toString()
} else {
IconsUtil.getBlockchainIconUri(blockchain)
getIconUrl(blockchain.toNetworkId())
}
imageView.setImageDrawable(null)
@ -39,7 +41,7 @@ fun Picasso.loadCurrenciesIcon(
}
url != null -> {
if (token != null) {
setTokenImage(imageView, textView, token)
setTokenImage(imageView, textView, token, blockchain)
}
this.load(url)
.transform(RoundedCornersTransform())
@ -76,7 +78,7 @@ private fun setOfflineCurrencyImage(
) {
when (token) {
null -> setBlockchainImage(imageView, textView, blockchain)
else -> setTokenImage(imageView, textView, token)
else -> setTokenImage(imageView, textView, token, blockchain)
}
}
@ -85,7 +87,7 @@ private fun setBlockchainImage(
textView: TextView,
blockchain: Blockchain,
) {
imageView.setImageResource(blockchain.getIconRes())
imageView.setImageResource(blockchain.getRoundIconRes())
imageView.colorFilter = null
if (blockchain.isTestnet()) imageView.saturation = 0f
textView.text = null
@ -95,9 +97,10 @@ private fun setTokenImage(
imageView: ImageFilterView,
textView: TextView,
token: Token,
tokenBlockchain: Blockchain
) {
imageView.setImageResource(R.drawable.shape_circle)
if (token.blockchain.isTestnet()) {
if (tokenBlockchain.isTestnet()) {
imageView.saturation = 0f
} else {
imageView.setColorFilter(token.getColor())

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

@ -6,6 +6,7 @@ import com.tangem.common.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.ThrottlerWithValues
import com.tangem.tap.common.extensions.dispatchOnMain
@ -19,6 +20,7 @@ import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
@ -46,6 +48,7 @@ class TapWalletManager {
suspend fun loadWalletData(walletManager: WalletManager) {
val blockchain = walletManager.wallet.blockchain
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val result = if (walletManagersThrottler.isStillThrottled(blockchain)) {
walletManagersThrottler.geValue(blockchain)!!
} else {
@ -54,21 +57,26 @@ class TapWalletManager {
when (result) {
is Result.Success -> {
checkForRentWarning(walletManager)
dispatchOnMain(WalletAction.LoadWallet.Success(result.data))
dispatchOnMain(WalletAction.LoadWallet.Success(result.data, blockchainNetwork))
}
is Result.Failure -> {
when (result.error) {
is TapError.WalletManagerUpdate.NoAccountError -> {
dispatchOnMain(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
))
dispatchOnMain(
WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
)
)
}
else -> {
dispatchOnMain(WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
))
dispatchOnMain(
WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
)
)
}
}
}
@ -85,8 +93,8 @@ class TapWalletManager {
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
val currencies = wallet.getTokens()
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
loadFiatRate(fiatCurrency, currencies)
}
@ -130,7 +138,8 @@ class TapWalletManager {
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (blockchain == Blockchain.Bitcoin
|| data.walletData?.blockchain == Blockchain.Bitcoin.id) {
|| data.walletData?.blockchain == Blockchain.Bitcoin.id
) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
@ -161,8 +170,10 @@ class TapWalletManager {
loadMultiWalletData(data, blockchain, primaryWalletManager)
} else {
dispatchOnMain(
WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager),
WalletAction.MultiWallet.AddBlockchains(listOf(blockchain))
WalletAction.MultiWallet.AddBlockchains(
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
listOf(primaryWalletManager)
)
)
}
} else {
@ -177,32 +188,40 @@ class TapWalletManager {
}
private suspend fun loadMultiWalletData(
scanResponse: ScanResponse, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
scanResponse: ScanResponse,
primaryBlockchain: Blockchain?,
primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
val savedCurrencies = currenciesRepository.loadCardCurrencies(scanResponse.card.cardId)
val savedCurrencies = currenciesRepository.loadSavedCurrencies(scanResponse.card.cardId, scanResponse.card.derivationStyle)
if (savedCurrencies == null) {
if (savedCurrencies.isEmpty()) {
if (primaryBlockchain != null && primaryWalletManager != null) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(primaryWalletManager)
dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(blockchains = listOf(primaryBlockchain), tokens = primaryTokens)
WalletAction.MultiWallet.SaveCurrencies(listOf(blockchainNetwork)),
WalletAction.MultiWallet.AddBlockchains(
listOf(blockchainNetwork),
listOf(primaryWalletManager)
),
WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager),
WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)),
WalletAction.MultiWallet.AddTokens(primaryTokens.toList())
WalletAction.MultiWallet.AddTokens(primaryTokens.toList(), blockchainNetwork)
)
} else {
val blockchains = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList())
val derivationStyle = scanResponse.card.derivationStyle
val blockchainNetworks = listOf(
BlockchainNetwork(Blockchain.Bitcoin, scanResponse.card),
BlockchainNetwork(Blockchain.Ethereum, scanResponse.card)
)
val walletManagers = walletManagerFactory.makeWalletManagersForApp(
scanResponse,
blockchainNetworks
)
dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(CardCurrencies(
blockchains = blockchains,
tokens = emptyList()
)),
WalletAction.MultiWallet.AddWalletManagers(walletManagers),
WalletAction.MultiWallet.AddBlockchains(blockchains.toList()),
WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks),
WalletAction.MultiWallet.AddBlockchains(blockchainNetworks, walletManagers),
)
}
dispatchOnMain(
@ -210,22 +229,25 @@ class TapWalletManager {
WalletAction.MultiWallet.FindTokensInUse,
)
} else {
val blockchains = savedCurrencies.blockchains.toList()
val walletManagers = if (
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
primaryWalletManager != null &&
primaryBlockchain != null
) {
val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchainsWithoutPrimary)
.plus(primaryWalletManager)
val blockchainsWithoutPrimary = savedCurrencies.filterNot { it.blockchain == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(
scanResponse,
blockchainsWithoutPrimary
).plus(primaryWalletManager)
} else {
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains)
walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies)
}
dispatchOnMain(
WalletAction.MultiWallet.AddWalletManagers(walletManagers),
WalletAction.MultiWallet.AddBlockchains(blockchains),
WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens.toList()),
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
}
}
}
@ -269,10 +291,12 @@ class TapWalletManager {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance < rentExempt
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean =
balance < rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val outgoingTxs =
walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
@ -284,11 +308,13 @@ class TapWalletManager {
if (!show) return
val currency = walletManager.wallet.blockchain.currency
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = walletManager.wallet.blockchain,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
dispatchOnMain(
WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
)
)
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}

View file

@ -0,0 +1,91 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.isTangemTwin
import java.util.*
object TapWorkarounds {
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
}
val Card.isStart2Coin: Boolean
get() = isStart2CoinIssuer(issuer.name)
val Card.isTestCard: Boolean
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
val Card.useOldStyleDerivation: Boolean
get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95"
val Card.derivationStyle: DerivationStyle?
get() = if (!settings.isHDWalletAllowed) {
null
} else if (useOldStyleDerivation) {
DerivationStyle.LEGACY
} else {
DerivationStyle.NEW
}
fun Card.isExcluded(): Boolean {
val excludedBatch = excludedBatches.contains(batchId)
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
return excludedBatch || excludedIssuerName
}
fun Card.isNotSupportedInThatRelease(): Boolean {
return false
}
@Deprecated("Use ScanResponse.isTangemNote")
fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId)
fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId)
fun getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.batchId]
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
private val excludedBatches = listOf(
"0027",
"0030",
"0031",
"0035"
)
private val excludedIssuers = listOf(
"TTM BANK"
)
private val tangemWalletBatches = listOf("AC01")
private val tangemNoteBatches = mapOf(
"AB01" to Blockchain.Bitcoin,
"AB02" to Blockchain.Ethereum,
"AB03" to Blockchain.CardanoShelley,
"AB04" to Blockchain.Dogecoin,
"AB05" to Blockchain.BSC,
"AB06" to Blockchain.XRP,
"AB07" to Blockchain.Bitcoin,
"AB08" to Blockchain.Ethereum,
)
}
val DELAY_SDK_DIALOG_CLOSE = 1400L
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !isTangemNote(this)
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -1,27 +1,32 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.common.*
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.Currency
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse,
blockchain: Blockchain
blockchain: Blockchain,
derivationParams: DerivationParams?
): WalletManager? {
val card = scanResponse.card
if (card.isTestCard && blockchain.getTestnetVersion() == null) return null
val supportedCurves = blockchain.getSupportedCurves() ?: return null
val supportedCurves = blockchain.getSupportedCurves()
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets) ?: return null
val environmentBlockchain = if (card.isTestCard) blockchain.getTestnetVersion()!! else blockchain
val environmentBlockchain =
if (card.isTestCard) blockchain.getTestnetVersion()!! else blockchain
val seedKey = wallet.extendedPublicKey
return when {
@ -32,16 +37,21 @@ fun WalletManagerFactory.makeWalletManagerForApp(
environmentBlockchain, wallet.curve
)
}
seedKey != null -> {
seedKey != null && derivationParams != null -> {
val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()]
val derivedKey = derivedKeys?.get(blockchain.derivationPath())
val derivationPath = when (derivationParams) {
is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style)
is DerivationParams.Custom -> derivationParams.path
}
val derivedKey = derivedKeys?.get(derivationPath)
?: return null
makeWalletManager(
cardId = card.cardId,
blockchain = environmentBlockchain,
seedKey = wallet.publicKey,
derivedKey = derivedKey
derivedKey = derivedKey,
derivation = derivationParams
)
}
else -> {
@ -55,12 +65,44 @@ fun WalletManagerFactory.makeWalletManagerForApp(
}
}
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork
): WalletManager? {
return makeWalletManagerForApp(
scanResponse,
blockchain = blockchainNetwork.blockchain,
derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card)
)
}
private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? {
return derivationPath?.let {
DerivationParams.Custom(
DerivationPath(it)
)
} ?: if (!card.settings.isHDWalletAllowed) {
null
} else if (card.useOldStyleDerivation) {
DerivationParams.Default(DerivationStyle.LEGACY)
} else {
DerivationParams.Default(DerivationStyle.NEW)
}
}
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse, currency: Currency
): WalletManager? {
return makeWalletManagerForApp(
scanResponse,
blockchain = currency.blockchain,
derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card)
)
}
fun WalletManagerFactory.makeWalletManagersForApp(
scanResponse: ScanResponse, blockchains: List<Blockchain>,
scanResponse: ScanResponse, blockchains: List<BlockchainNetwork>,
): List<WalletManager> {
val isTestCard = scanResponse.card.isTestCard
val filteredBlockchains = blockchains.mapNotNull { if (isTestCard) it.getTestnetVersion() else it }
return filteredBlockchains.mapNotNull { makeWalletManagerForApp(scanResponse, it) }
return blockchains.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
}
fun WalletManagerFactory.makePrimaryWalletManager(
@ -71,7 +113,12 @@ fun WalletManagerFactory.makePrimaryWalletManager(
} else {
scanResponse.getBlockchain()
}
return makeWalletManagerForApp(scanResponse, blockchain)
val derivationParams = getDerivationParams(null, scanResponse.card)
return makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = blockchain,
derivationParams = derivationParams
)
}
private fun selectWallet(wallets: List<CardWallet>): CardWallet? {

View file

@ -20,6 +20,10 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.product.CreateWalletsTask
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
@ -177,7 +181,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
val blockchainsForCurve = getBlockchains(response.cardId).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath() }
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) }
if (derivationPaths.isNotEmpty()) {
map[response.wallet.publicKey.toMapKey()] = derivationPaths
}

View file

@ -1,10 +1,12 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.card.WalletData
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
@ -25,11 +27,57 @@ import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.preferencesStorage
data class 1ScanResponse(
val card: Card,
val productType: ProductType,
val walletData: WalletData?,
val secondTwinPublicKey: String? = null,
val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
val primaryCard: PrimaryCard? = null
) : CommandResponse {
fun getBlockchain(): Blockchain {
if (productType == ProductType.Note) return getTangemNoteBlockchain(card)
?: return Blockchain.Unknown
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
return Blockchain.fromId(blockchainName)
}
fun getPrimaryToken(): Token? {
val cardToken = walletData?.token ?: return null
return Token(
cardToken.name,
cardToken.symbol,
cardToken.contractAddress,
cardToken.decimals,
)
}
fun isTangemNote(): Boolean = productType == ProductType.Note
fun isTangemWallet(): Boolean = productType == ProductType.Wallet
fun isTangemTwins(): Boolean = productType == ProductType.Twins
fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
fun supportsBackup(): Boolean = card.settings.isBackupAllowed
fun twinsIsTwinned(): Boolean =
card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
}
typealias KeyWalletPublicKey = ByteArrayKey
class ScanProductTask(
val card: Card? = null,
private val currenciesRepository: CurrenciesRepository?,
@ -223,28 +271,38 @@ private class ScanWalletProcessor(
}
}
private fun getBlockchainsToDerive(card: Card): List<Blockchain> {
private fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.derivationStyle).toMutableList()
val blockchainsToDerive = if (cardCurrencies == null) {
mutableListOf(Blockchain.Bitcoin, Blockchain.Ethereum)
} else {
val tokenBlockchains = cardCurrencies.tokens.map { it.blockchain }
(cardCurrencies.blockchains + tokenBlockchains).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card))
}
if (card.settings.isHDWalletAllowed) {
blockchainsToDerive.addAll(
listOf(
Blockchain.Ethereum,
Blockchain.Binance,
Blockchain.EthereumTestnet
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.Binance, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card)
)
)
}
if (additionalBlockchainsToDerive != null) {
blockchainsToDerive.addAll(additionalBlockchainsToDerive)
blockchainsToDerive.addAll(additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) })
}
if (!card.useOldStyleDerivation) {
blockchainsToDerive.removeAll(
listOf(
Blockchain.BSC, Blockchain.BSCTestnet,
Blockchain.Polygon, Blockchain.PolygonTestnet,
Blockchain.RSK,
Blockchain.Fantom, Blockchain.FantomTestnet,
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
).map { BlockchainNetwork(it, card) }
)
}
return blockchainsToDerive.distinct()
}
@ -254,13 +312,13 @@ private class ScanWalletProcessor(
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
blockchains.forEach { blockchain ->
val curve = blockchain.getPrimaryCurve()
val curve = blockchain.blockchain.getPrimaryCurve()
val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
if (wallet.chainCode == null) return@forEach
val key = wallet.publicKey.toMapKey()
val path = blockchain.derivationPath()
val path = blockchain.derivationPath?.let { DerivationPath(it) }
if (path != null) {
val addedDerivations = derivations[key]
if (addedDerivations != null) {

View file

@ -7,15 +7,19 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.extensions.appendIf
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.TapWorkarounds.derivationStyle
import com.tangem.tap.domain.extensions.setCustomIconUrl
import com.tangem.tap.features.demo.DemoHelper
import timber.log.Timber
import java.util.*
class CurrenciesRepository(val context: Application) {
@ -29,52 +33,48 @@ 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()
if (DemoHelper.isDemoCardId(cardId)) {
blockchains.addAll(DemoHelper.config.demoBlockchains)
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
fun saveUpdatedCurrency(cardId: String, blockchainNetwork: BlockchainNetwork) {
var changed = false
val currencies = loadSavedCurrencies(cardId).map {
if (it == blockchainNetwork) {
changed = true
blockchainNetwork
} else {
it
}
}
if (blockchains.isEmpty()) return null
return CardCurrencies(loadSavedTokens(cardId), blockchains.toList())
val updatedCurrencies = if (changed) currencies else currencies + blockchainNetwork
saveCurrencies(cardId, updatedCurrencies.distinct())
}
fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) {
saveTokens(cardId, currencies.tokens)
saveBlockchains(cardId, currencies.blockchains)
fun removeToken(cardId: String, token: Token, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrencies(cardId).map {
if (it == blockchainNetwork) {
it.copy(tokens = it.tokens.filterNot { it == token })
} else {
it
}
}
saveCurrencies(cardId, currencies)
}
fun saveAddedToken(cardId: String, token: Token) {
val tokens = loadSavedTokens(cardId) + token
saveTokens(cardId, tokens)
}
fun saveAddedTokens(cardId: String, tokens: Collection<Token>) {
saveTokens(cardId, loadSavedTokens(cardId) + tokens.distinct())
}
fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId) + blockchain
saveBlockchains(cardId, blockchains.distinct())
}
fun removeToken(cardId: String, token: Token) {
val tokens = loadSavedTokens(cardId).filterNot { it == token }
saveTokens(cardId, tokens)
}
fun removeBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }
saveBlockchains(cardId, blockchains)
fun removeBlockchain(cardId: String, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrencies(cardId).filterNot { it == blockchainNetwork }
saveCurrencies(cardId, currencies)
}
fun removeCurrencies(cardId: String) {
saveTokens(cardId, emptyList())
saveBlockchains(cardId, emptyList())
saveCurrencies(cardId, emptyList())
}
private fun loadSavedTokens(cardId: String): List<Token> {
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedTokens(cardId: String): List<TokenDao> {
val json = try {
context.readFileText(getFileNameForTokens(cardId))
} catch (exception: Exception) {
@ -82,17 +82,13 @@ class CurrenciesRepository(val context: Application) {
}
return try {
tokensAdapter.fromJson(json)!!.map { it.toToken() }.distinct()
tokensAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
private fun saveTokens(cardId: String, tokens: List<Token>) {
val json = tokensAdapter.toJson(tokens.distinct().map { TokenDao.fromToken(it) })
context.rewriteFile(json, getFileNameForTokens(cardId))
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
@ -102,8 +98,61 @@ class CurrenciesRepository(val context: Application) {
}
}
private fun saveBlockchains(cardId: String, blockchains: List<Blockchain>) {
val json = blockchainsAdapter.toJson(blockchains.distinct())
fun loadSavedCurrencies(
cardId: String,
derivationStyle: DerivationStyle? = null
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies(cardId)
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: loadSavedCurrenciesOldWay(
cardId,
derivationStyle
)
} catch (exception: Exception) {
emptyList()
}
}
private fun loadDemoCurrencies(cardId: String): List<BlockchainNetwork> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList()
)
}
}
private fun loadSavedCurrenciesOldWay(
cardId: String, derivationStyle: DerivationStyle?
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val currencies = getSupportedTokens()
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = tokens
.filter { it.blockchainDao.toBlockchain() == blockchain }
.map {
val token = it.toToken()
val id = currencies
.find { it.contracts?.find { it.address == token.contractAddress } != null }
?.id
token.copy(id = id)
}
)
}
saveCurrencies(cardId, blockchainNetworks) // migrate saved currencies
return blockchainNetworks
}
fun saveCurrencies(cardId: String, currencies: List<BlockchainNetwork>) {
val json = blockchainNetworkAdapter.toJson(currencies)
context.rewriteFile(json, getFileNameForBlockchains(cardId))
}
@ -116,18 +165,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 +183,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()
@ -152,17 +193,6 @@ class CurrenciesRepository(val context: Application) {
return obsoleteTokensAdapter.fromJson(tokenJson)!!.map { it.toTokenDao(blockchain) }
}
private fun getSupportedTokens(): List<Blockchain> {
return listOf(
Blockchain.Ethereum,
Blockchain.BSC,
Blockchain.Binance,
Blockchain.Polygon,
Blockchain.Avalanche,
Blockchain.Fantom,
)
}
fun getBlockchains(
cardFirmware: FirmwareVersion,
isTestNet: Boolean = false
@ -178,9 +208,11 @@ class CurrenciesRepository(val context: Application) {
// Use this list to temporarily exclude a blockchain from the list of tokens.
private fun excludeUnsupportedBlockchains(blockchains: List<Blockchain>): List<Blockchain> {
return blockchains.toMutableList().apply {
removeAll(listOf(
removeAll(
listOf(
// Any blockchain
))
)
)
}
}
@ -218,24 +250,11 @@ data class TokenDao(
symbol = symbol,
contractAddress = contractAddress,
decimals = decimalCount,
blockchain = blockchainDao.toBlockchain()
).apply {
customIconUrl?.let { this.setCustomIconUrl(it) }
}
}
companion object {
fun fromToken(token: Token): TokenDao {
return TokenDao(
name = token.name,
symbol = token.symbol,
contractAddress = token.contractAddress,
decimalCount = token.decimals,
blockchainDao = BlockchainDao.fromBlockchain(token.blockchain),
customIconUrl = token.getCustomIconUrl()
)
}
}
}
@JsonClass(generateAdapter = true)
@ -280,29 +299,78 @@ data class ObsoleteTokenDao(
}
}
@JsonClass(generateAdapter = true)
data class CardCurrenciesDao(
val tokens: List<TokenDao>,
val blockchains: List<Blockchain>,
) {
fun toCardCurrencies(): CardCurrencies {
return CardCurrencies(
tokens = tokens.map { it.toToken() }.distinct(),
blockchains = blockchains
)
}
companion object {
fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao {
return CardCurrenciesDao(
tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.distinct(),
blockchains = cardCurrencies.blockchains
)
}
}
}
//@JsonClass(generateAdapter = true)
//data class CardCurrenciesDao(
// val tokens: List<TokenDao>,
// val blockchains: List<Blockchain>,
//) {
// fun toCardCurrencies(): CardCurrencies {
// return CardCurrencies(
// tokens = tokens.map { it.toToken() }.distinct(),
// blockchains = blockchains
// )
// }
//
// companion object {
// fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao {
// return CardCurrenciesDao(
// tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.distinct(),
// blockchains = cardCurrencies.blockchains
// )
// }
// }
//}
data class CardCurrencies(
val tokens: List<Token>,
val blockchains: List<Blockchain>,
)
)
@JsonClass(generateAdapter = true)
data class BlockchainNetwork(
val blockchain: Blockchain,
val derivationPath: String?,
val tokens: List<Token>
) {
constructor(blockchain: Blockchain, card: Card) : this(
blockchain = blockchain,
derivationPath = if (card.settings.isHDWalletAllowed) blockchain.derivationPath(card.derivationStyle)?.rawPath else null,
tokens = emptyList()
)
fun updateTokens(tokens: List<Token>): BlockchainNetwork {
return copy(
tokens = (this.tokens + tokens).distinct()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BlockchainNetwork
if (blockchain != other.blockchain) return false
if (derivationPath != other.derivationPath) return false
return true
}
override fun hashCode(): Int {
var result = blockchain.hashCode()
result = 31 * result + (derivationPath?.hashCode() ?: 0)
return result
}
companion object {
fun fromWalletManager(walletManager: WalletManager): BlockchainNetwork {
return BlockchainNetwork(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
walletManager.cardTokens.toList()
)
}
}
}

View file

@ -0,0 +1,135 @@
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 mapNotNull { 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? {
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: return null
return Contract(
networkId = contract.networkId,
blockchain = blockchain,
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 -> null
}
}
fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalaunche"
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 -> "rootstock"
Blockchain.Stellar -> "stellar"
Blockchain.StellarTestnet -> "stellar"
Blockchain.Solana -> "solana"
Blockchain.SolanaTestnet -> "solana"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "ripple"
}
}

View file

@ -36,7 +36,7 @@ internal class DemoOnboardingNoteMiddleware : DemoMiddleware {
val balanceAmount = config.getBalance(walletManager.wallet.blockchain)
val loadedBalance = noteState.walletBalance.copy(
value = balanceAmount.value!!,
currency = Currency.Blockchain(walletManager.wallet.blockchain),
currency = Currency.Blockchain(walletManager.wallet.blockchain, null),
state = ProgressState.Done,
error = null,
criticalError = null

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
@ -14,10 +16,14 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -164,14 +170,16 @@ class WalletConnectMiddleware {
chainId = chainId, peer = session.peerMeta
) ?: Blockchain.Ethereum
store.dispatch(GlobalAction.ScanCard(additionalBlockchainsToDerive = listOf(blockchain),
onSuccess = { scanResponse ->
handleScanResponse(scanResponse, session, blockchain)
},
onFailure = {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
}, R.string.wallet_connect_scan_card_message
)
store.dispatch(
GlobalAction.ScanCard(
additionalBlockchainsToDerive = listOf(blockchain),
onSuccess = { scanResponse ->
handleScanResponse(scanResponse, session, blockchain)
},
onFailure = {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
}, R.string.wallet_connect_scan_card_message
)
)
}
@ -229,22 +237,35 @@ class WalletConnectMiddleware {
}
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(blockchainToMake)
?: factory.makeWalletManagerForApp(scanResponse, blockchainToMake)
val derivationPath = blockchainToMake.derivationPath(card.derivationStyle)?.rawPath
store.state.walletState.getWalletManager(
Currency.Blockchain(blockchainToMake, derivationPath)
)
?: factory.makeWalletManagerForApp(
scanResponse,
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
?.also { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
BlockchainNetwork.fromWalletManager(walletManager), walletManager
)
)
}
} else {
val walletManager = factory.makeWalletManagerForApp(scanResponse, blockchainToMake)
if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains(
blockchainToMake
) != true
val walletManager = factory.makeWalletManagerForApp(
scanResponse,
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.derivationStyle)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {
currenciesRepository.saveAddedBlockchain(
currenciesRepository.saveUpdatedCurrency(
card.cardId,
blockchainToMake
BlockchainNetwork.fromWalletManager(walletManager)
)
}
}

View file

@ -106,6 +106,7 @@ class AdditionalEmailInfo {
var address: String = "",
var explorerLink: String = "",
var host: String = "",
var derivationPath: String = "",
)
var appVersion: String = ""
@ -159,7 +160,8 @@ class AdditionalEmailInfo {
blockchain = manager.wallet.blockchain,
address = getAddress(manager.wallet),
explorerLink = getExploreUri(manager.wallet),
host = manager.currentHost
host = manager.currentHost,
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
)
)
if (manager.cardTokens.isNotEmpty()) {
@ -173,7 +175,8 @@ class AdditionalEmailInfo {
blockchain = wallet.blockchain,
address = getAddress(wallet),
explorerLink = getExploreUri(wallet),
host = host
host = host,
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
)
this.destinationAddress = destinationAddress
@ -325,6 +328,7 @@ class EmailDataBuilder(
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Explorer link", it.explorerLink)
infoHolder.tokens[it.blockchain]?.let { tokens ->
@ -332,6 +336,7 @@ class EmailDataBuilder(
appendLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
if (token.id != null) builder.appendKeyValue("Id", token.id ?: "")
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
@ -342,6 +347,7 @@ class EmailDataBuilder(
fun appendTxFailedBlockchainInfo(error: String): EmailDataBuilder {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Token", infoHolder.token)
builder.appendKeyValue("Error", error)

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,9 +19,13 @@ 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 com.tangem.tap.common.compose.SpacerS24
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
@ -28,7 +35,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
@ -78,7 +86,8 @@ fun StoriesScreen(
}
)
Box(
Modifier.weight(1f)
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(isPressed) {
detectTapGestures(
@ -107,7 +116,7 @@ fun StoriesScreen(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
) {
SpacerS24()
StoriesProgressBar(
@ -138,15 +147,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
)
}
}
}
@ -154,5 +192,5 @@ fun StoriesScreen(
@Preview
@Composable
fun InstagramScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {})
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {})
}

View file

@ -67,7 +67,9 @@ class OnboardingManager(
}
}
return balance.copy(currency = Currency.Blockchain(walletManager.wallet.blockchain))
return balance.copy(currency = Currency.Blockchain(
walletManager.wallet.blockchain, walletManager.wallet.publicKey.derivationPath?.rawPath)
)
}
fun activationStarted(cardId: String) {
@ -81,7 +83,7 @@ class OnboardingManager(
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown),
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown, null),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,

View file

@ -113,7 +113,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = noteState.walletBalance.copy(
currency = Currency.Blockchain(walletManager.wallet.blockchain),
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath
),
state = ProgressState.Loading,
error = null,
criticalError = null

View file

@ -215,7 +215,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
val isLoadedBefore = twinCardsState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = twinCardsState.walletBalance.copy(
currency = Currency.Blockchain(walletManager.wallet.blockchain),
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath
),
state = ProgressState.Loading,
error = null,
criticalError = null

View file

@ -24,6 +24,7 @@ import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*
@ -203,11 +204,11 @@ private fun sendTransaction(
}
scope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
dispatch(WalletAction.LoadWallet(walletManager.wallet.blockchain))
dispatch(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
}
delay(11000) // more than 10000 to avoid throttling
withContext(Dispatchers.Main) {
dispatch(WalletAction.LoadWallet(walletManager.wallet.blockchain))
dispatch(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
}
}
}

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.DerivationStyle
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.Action
@ -8,14 +10,19 @@ sealed class TokensAction : Action {
object ResetState : TokensAction()
object LoadCurrencies : TokensAction() {
data class Success(val currencies: List<CurrencyListItem>) : TokensAction()
data class AllowToAddTokens(val allow: Boolean) : TokensAction()
data class LoadCurrencies(val supportedBlockchains: List<Blockchain>? = null) : TokensAction() {
data class Success(val currencies: List<Currency>) : TokensAction()
}
data class SetAddedCurrencies(val wallets: List<WalletData>) : TokensAction()
data class SetAddedCurrencies(
val wallets: List<WalletData>, val derivationStyle: DerivationStyle?
) : 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<TokenWithBlockchain>,
val addedBlockchains: List<Blockchain>
) : TokensAction()
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
@ -18,6 +19,13 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -29,7 +37,7 @@ class TokensMiddleware {
{ next ->
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> handleLoadCurrencies()
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action)
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
next(action)
@ -37,35 +45,41 @@ class TokensMiddleware {
}
}
private fun handleLoadCurrencies() {
val scanResponse = store.state.globalState.scanResponse ?: return
val isTestcard = scanResponse.card.isTestCard
private fun handleLoadCurrencies(action: TokensAction.LoadCurrencies) {
val scanResponse = store.state.globalState.scanResponse
val isTestcard = scanResponse?.card?.isTestCard ?: false
val currencies = currenciesRepository.getSupportedTokens(isTestcard)
.filter(action.supportedBlockchains?.toSet())
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(
store.state.tokensState.derivationStyle
)
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.token) }
val tokensToRemove = currentTokens.filter { token -> !action.addedTokens.any { it.token == token } }
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, scanResponse)
store.dispatch(NavigationAction.PopBackTo())
}
}
@ -73,7 +87,7 @@ class TokensMiddleware {
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
blockchains: List<Blockchain>,
tokens: List<Token>
tokens: List<TokenWithBlockchain>
) {
val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, blockchains, tokens),
@ -104,7 +118,7 @@ class TokensMiddleware {
derivedKeys = updatedDerivedKeys
)
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
submitAdd(blockchains, tokens)
submitAdd(blockchains, tokens, scanResponse)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.PopBackTo())
@ -120,13 +134,12 @@ class TokensMiddleware {
curve: EllipticCurve,
scanResponse: ScanResponse,
blockchains: List<Blockchain>,
tokens: List<Token>
tokens: List<TokenWithBlockchain>
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val tokenBlockchains = tokens.map { it.blockchain }
val derivationPathsCandidates = (blockchains + tokenBlockchains).distinct()
.mapNotNull { it.derivationPath() }
val derivationPathsCandidates = (blockchains + tokens.map { it.blockchain }).distinct()
.mapNotNull { it.derivationPath(scanResponse.card.derivationStyle) }
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
@ -147,11 +160,38 @@ class TokensMiddleware {
val mapKeyOfWalletPublicKey: ByteArrayKey
)
private fun submitAdd(blockchains: List<Blockchain>, tokens: List<Token>) {
(blockchains.map {
WalletAction.MultiWallet.AddBlockchain(it)
private fun submitAdd(
blockchains: List<Blockchain>, tokens: List<TokenWithBlockchain>, scanResponse: ScanResponse,
) {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
(blockchains.mapNotNull {
val walletManager = factory.makeWalletManagerForApp(
scanResponse, it,
scanResponse.card.derivationStyle?.let { DerivationParams.Default(it) }
) ?: return@mapNotNull null
WalletAction.MultiWallet.AddBlockchain(BlockchainNetwork.fromWalletManager(walletManager), walletManager)
} + tokens.map {
WalletAction.MultiWallet.AddToken(it)
val blockchainNetwork = BlockchainNetwork(it.blockchain, scanResponse.card)
WalletAction.MultiWallet.AddToken(it.token, blockchainNetwork)
}).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,27 @@ 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(action.derivationStyle),
addedTokens = action.wallets.toTokensWithBlockchains(action.derivationStyle),
addedWallets = action.wallets,
derivationStyle = action.derivationStyle
)
}
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(tokensState.derivationStyle),
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,66 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
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<TokenWithBlockchain> = 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,
val derivationStyle: DerivationStyle? = null
) : 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>.toTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
return mapNotNull {
if (it.currency !is com.tangem.tap.features.wallet.redux.Currency.Token) return@mapNotNull null
if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(it.currency.token, it.currency.blockchain)
}.distinct()
}
fun List<WalletData>.toBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return mapNotNull {
if (it.currency.isCustomCurrency(derivationStyle)) {
null
} else {
(it.currency as? com.tangem.tap.features.wallet.redux.Currency.Blockchain)?.blockchain
}
}.distinct()
}
data class TokenWithBlockchain(
val token: Token,
val blockchain: Blockchain
)
fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency> {
if (supportedBlockchains == null) return this
return map {
it.copy(contracts =
it.contracts?.filter {
supportedBlockchains.contains(it.blockchain) && it.blockchain.canHandleTokens()
}
)
}.filterNot {
it.contracts.isNullOrEmpty() && !supportedBlockchains.contains(Blockchain.fromNetworkId(it.id))
}
}

View file

@ -8,15 +8,22 @@ 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.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
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 +34,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 +72,32 @@ 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()
val onSaveChanges = { tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain> ->
store.dispatch(TokensAction.SaveChanges(tokens, blockchains))
}
val onNetworkItemClicked = { contractAddress: ContractAddress ->
context?.copyToClipboard(contractAddress)
store.dispatchNotification(R.string.contract_address_copied_message)
}
rvPopularTokens.layoutManager = LinearLayoutManager(context)
rvPopularTokens.adapter = viewAdapter
cvCurrencies.setContent {
AppCompatTheme {
CurrenciesScreen(
tokensState = tokensState,
searchInput = searchInput,
onSaveChanges = onSaveChanges,
onNetworkItemClicked = onNetworkItemClicked
)
}
}
}
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 +115,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,127 @@
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.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.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.wallet.R
@Composable
fun CollapsedCurrencyItem(
currency: Currency,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
onCurrencyClick: (String) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { onCurrencyClick(currency.id) })
) {
val blockchain = Blockchain.fromNetworkId(currency.id)
val iconRes = currency.iconUrl
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.isNullOrEmpty()) {
currency.contracts.map { contract ->
if (contract.address == currency.symbol) {
BlockchainNetworkItem(
blockchain = Blockchain.fromNetworkId(contract.networkId),
addedBlockchains = addedBlockchains
)
} else {
val added =
addedTokens.map { it.token.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 {
BlockchainNetworkItem(
blockchain = blockchain,
addedBlockchains = addedBlockchains
)
}
}
}
Image(
painter = painterResource(id = R.drawable.ic_arrow_collapsed),
contentDescription = null,
Modifier
.padding(20.dp)
.align(Alignment.CenterVertically)
)
}
}
@Composable
fun BlockchainNetworkItem(
blockchain: Blockchain?,
addedBlockchains: List<Blockchain>
) {
val added = addedBlockchains.contains(blockchain)
val icon =
if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes()
if (icon != null) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
Spacer(modifier = Modifier.size(5.dp))
}
}

View file

@ -0,0 +1,109 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.layout.Column
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.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.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
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<TokenWithBlockchain>, List<Blockchain>) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
val isKeyboardOpen by keyboardAsState()
val onAddCurrencyToggleClick = { currency: Currency, token: TokenWithBlockchain? ->
if (token != null) {
val mutableList = addedTokensState.value.toMutableList()
if (mutableList.contains(token)) {
mutableList.remove(token)
} else {
mutableList.add(token)
}
addedTokensState.value = mutableList
} else {
val blockchain = Blockchain.fromNetworkId(currency.id)
val mutableList = addedBlockchainsState.value.toMutableList()
if (mutableList.contains(blockchain)) {
mutableList.remove(blockchain)
} else {
blockchain?.let { mutableList.add(blockchain) }
}
addedBlockchainsState.value = mutableList
}
}
Scaffold(
// topBar =,
floatingActionButton = {
if (tokensState.value.allowToAdd) SaveChangesButton(isKeyboardOpen) {
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
}
},
floatingActionButtonPosition = FabPosition.Center,
) {
Column {
if (tokensState.value.allowToAdd) CurrenciesWarning()
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,
onNetworkItemClicked = onNetworkItemClicked
)
}
}
}
@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,53 @@
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.RoundedCornerShape
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.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
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
import com.tangem.wallet.R
@Composable
fun CurrenciesWarning() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(Color(0xFFF2F2F2), shape = RoundedCornerShape(10.dp))
)
{
val warning = stringResource(id = R.string.alert_manage_tokens_addresses_message)
val end = warning.indexOf(" ")
val spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold),
start = 0,
end = end
)
)
Text(
text = AnnotatedString(text = warning, spanStyles = spanStyles),
textAlign = TextAlign.Center,
color = Color(0xFF848488),
modifier = Modifier.padding(
top = 8.dp,
bottom = 8.dp,
start = 16.dp,
end = 16.dp
),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.runtime.Composable
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@Composable
fun CurrencyItem(
currency: Currency,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
expanded: Boolean,
onCurrencyClick: (String) -> Unit,
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
if (expanded) {
ExpandedCurrencyItem(
currency = currency,
nonRemovableTokens = nonRemovableTokens,
nonRemovableBlockchains = nonRemovableBlockchains,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,
onCurrencyClick = onCurrencyClick, onAddCurrencyToggled = onAddCurrencyToggled,
onNetworkItemClicked = onNetworkItemClicked
)
} 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,159 @@
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.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.wallet.R
@Composable
fun ExpandedCurrencyItem(
currency: Currency,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
onCurrencyClick: (String) -> Unit,
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
val blockchain = Blockchain.fromNetworkId(currency.id)
val iconRes = currency.iconUrl
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 } ?: listOfNotNull(
Blockchain.fromNetworkId(currency.id)
)
Row {
Box(
modifier = Modifier
.fillMaxHeight()
) {
if (blockchains.size > 1) {
val dividerHeight = if (blockchains.size == 2) {
60
} else if (blockchains.size == 3) {
110
} else {
43 * blockchains.size
}
Divider(
color = Color(0xFFDEDEDE),
modifier = Modifier
.height(dividerHeight.dp)
.padding(start = 37.5.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 && contract.address != currency.symbol) {
addedTokens.map { it.token.contractAddress }.contains(contract.address)
} else {
addedBlockchains.contains(blockchain)
}
val canBeRemoved = if (contract != null && contract.address != currency.symbol) {
!nonRemovableTokens.contains(contract.address)
} else {
!nonRemovableBlockchains.contains(blockchain)
}
NetworkItem(
currency = currency,
contract = contract,
blockchain = blockchain, allowToAdd = allowToAdd,
added = added,
canBeRemoved = canBeRemoved,
onAddCurrencyToggled = onAddCurrencyToggled,
onNetworkItemClicked = onNetworkItemClicked,
)
}
}
}
}
}

View file

@ -0,0 +1,69 @@
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.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@Composable
fun ListOfCurrencies(
currencies: List<Currency>,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
searchInput: String,
allowToAdd: Boolean,
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> 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,
onNetworkItemClicked = onNetworkItemClicked
)
}
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
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
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NetworkItem(
currency: Currency, contract: Contract?,
blockchain: Blockchain, allowToAdd: Boolean,
added: Boolean, canBeRemoved: Boolean,
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
val isBlockchain = contract == null || contract.address == currency.symbol
Row(
modifier = Modifier
.fillMaxWidth()
.combinedClickable {
if (contract != null) onNetworkItemClicked(contract.address)
}
) {
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 (isBlockchain) "MAIN" else blockchain.getNetworkName().uppercase(),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (!isBlockchain) Color(0xFF8E8E93) else Color(0xFF1ACE80),
)
}
if (allowToAdd) {
val token = if (!isBlockchain) {
Token(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
contractAddress = contract!!.address,
decimals = contract.decimalCount,
)
} else {
null
}
val tokenWithBlockchain =
token?.let { TokenWithBlockchain(token, contract!!.blockchain) }
val currencyToSave = if (isBlockchain && contract != null) {
currency.copy(id = contract.networkId)
} else {
currency
}
Switch(
checked = added,
enabled = canBeRemoved,
onCheckedChange = { onAddCurrencyToggled(currencyToSave, tokenWithBlockchain) },
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFF1ACE80)
)
)
}
}
}

View file

@ -8,7 +8,7 @@ import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -24,9 +24,14 @@ sealed class WalletAction : Action {
}
data class LoadWallet(val blockchain: Blockchain? = null) : WalletAction() {
data class Success(val wallet: Wallet) : WalletAction()
data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction()
data class LoadWallet(val blockchain: BlockchainNetwork? = null) : WalletAction() {
data class Success(val wallet: Wallet, val blockchain: BlockchainNetwork) : WalletAction()
data class NoAccount(
val wallet: Wallet,
val blockchain: BlockchainNetwork,
val amountToCreateAccount: String
) : WalletAction()
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
}
@ -35,19 +40,30 @@ sealed class WalletAction : Action {
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet() {
constructor(walletManager: WalletManager) : this(listOf(walletManager))
}
data class AddBlockchain(val blockchain: Blockchain) : MultiWallet()
data class AddBlockchains(val blockchains: List<Blockchain>) : MultiWallet()
data class AddTokens(val tokens: List<Token>) : MultiWallet()
data class AddToken(val token: Token) : MultiWallet()
data class SaveCurrencies(val cardCurrencies: CardCurrencies) : MultiWallet()
data class AddBlockchain(
val blockchain: BlockchainNetwork,
val walletManager: WalletManager?
) : MultiWallet()
data class AddBlockchains(
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>
) : MultiWallet()
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork) :
MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
data class SaveCurrencies(val blockchainNetworks: List<BlockchainNetwork>) : MultiWallet()
object FindTokensInUse : MultiWallet()
object FindBlockchainsInUse : MultiWallet()
data class TokenLoaded(val amount: Amount, val token: Token) : MultiWallet()
data class TokenLoaded(
val amount: Amount,
val token: Token,
val blockchain: BlockchainNetwork
) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class RemoveWallet(val walletData: WalletData) : MultiWallet()
data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet()
@ -135,5 +151,9 @@ sealed class WalletAction : Action {
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class SetWalletRent(val blockchain: Blockchain, val minRent: String, val rentExempt: String) : WalletAction()
data class SetWalletRent(
val blockchain: BlockchainNetwork,
val minRent: String,
val rentExempt: String
) : WalletAction()
}

View file

@ -10,10 +10,12 @@ import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.TapWorkarounds.derivationStyle
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.domain.extensions.sellIsAllowed
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.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
@ -33,8 +35,7 @@ data class WalletState(
val hashesCountVerified: Boolean? = null,
val walletDialog: StateDialog? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val walletsData: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val wallets: List<WalletStore> = listOf(),
val isMultiwalletAllowed: Boolean = false,
val cardCurrency: CryptoCurrencyName? = null,
val selectedCurrency: Currency? = null,
@ -52,45 +53,96 @@ data class WalletState(
val isTangemTwins: Boolean
get() = store.state.globalState.scanResponse?.isTangemTwins() == true
val primaryWallet = if (walletsData.isNotEmpty()) walletsData[0] else null
val primaryWallet: WalletData? = if (wallets.isNotEmpty()) wallets[0].walletsData[0] else null
val primaryWalletManager: WalletManager? =
if (wallets.isNotEmpty()) wallets[0].walletManager else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val blockchains: List<Blockchain>
get() = walletManagers.map { it.wallet.blockchain }
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
val currencies: List<Currency>
get() = walletsData.mapNotNull { it.currency }
get() = wallets.flatMap { it.walletsData }.map { it.currency }
val walletsData: List<WalletData>
get() = wallets.flatMap { it.walletsData }
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
fun getWalletManager(token: Token?): WalletManager? {
if (token == null) return null
return walletManagers.find { it.wallet.blockchain == token.blockchain }
return wallets
.mapNotNull { it.walletManager }
.find { walletManager ->
walletManager.cardTokens.any { it.contractAddress == token.contractAddress }
}
}
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return walletManagers.find { it.wallet.blockchain == currency.blockchain }
return wallets.map { it.walletManager }
.find { it?.wallet?.blockchain == currency.blockchain }
}
fun getWalletManager(blockchain: Blockchain): WalletManager? {
return walletManagers.find { it.wallet.blockchain == blockchain }
fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? {
return wallets.find { it.blockchainNetwork == blockchain }?.walletManager
}
fun getWalletData(blockchain: BlockchainNetwork?): WalletData? {
if (blockchain == null) return null
return walletsData.find {
it.currency is Currency.Blockchain &&
it.currency.blockchain == blockchain.blockchain &&
it.currency.derivationPath == blockchain.derivationPath
}
}
fun getWalletStore(currency: Currency?): WalletStore? {
if (currency == null) return null
return wallets.firstOrNull {
it.blockchainNetwork.derivationPath == currency.derivationPath &&
(it.blockchainNetwork.blockchain == currency.blockchain)
}
}
fun getWalletStore(wallet: Wallet?): WalletStore? {
if (wallet == null) return null
val currency =
Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath)
return getWalletStore(currency)
}
fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? {
if (blockchainNetwork == null) return null
return wallets.firstOrNull {
it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath &&
(it.blockchainNetwork.blockchain == blockchainNetwork.blockchain)
}
}
fun getWalletData(currency: Currency?): WalletData? {
if (currency == null) return null
return walletsData.find { it.currency == currency }
}
fun getWalletData(blockchain: Blockchain?): WalletData? {
if (blockchain == null) return null
return walletsData.find { (it.currency as? Currency.Blockchain)?.blockchain == blockchain }
return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency }
}
fun getWalletData(token: Token?): WalletData? {
if (token == null) return null
return walletsData.find { (it.currency as? Currency.Token)?.token == token }
return walletsData.find {
(it.currency as? Currency.Token)?.token == token &&
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
}
}
fun getWalletData(blockchain: Blockchain?): WalletData? {
if (blockchain == null) return null
return walletsData.find {
(it.currency as? Currency.Blockchain)?.blockchain == blockchain &&
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
}
}
fun getSelectedWalletData(): WalletData? {
@ -114,36 +166,90 @@ data class WalletState(
if (walletData.currency is Currency.Blockchain) {
return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() &&
wallet.amounts.toSendableAmounts().isEmpty()
wallet.amounts.toSendableAmounts().isEmpty()
} else if (walletData.currency is Currency.Token) (
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
)
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address
).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
)
}
return false
}
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
return (walletData.currency is Currency.Blockchain &&
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|| (walletData.currency is Currency.Token &&
walletData.currency.token == store.state.walletState.primaryToken)
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|| (walletData.currency is Currency.Token &&
walletData.currency.token == store.state.walletState.primaryToken)
}
fun replaceWalletInWallets(walletData: WalletData?): List<WalletData> {
if (walletData == null) return walletsData
fun replaceWalletInWallets(wallet: WalletStore?): List<WalletStore> {
if (wallet == null) return wallets
var changed = false
val updatedWallets = walletsData.map {
if (it.currency == walletData.currency) {
val updatedWallets = wallets.map {
if (it.blockchainNetwork == wallet.blockchainNetwork) {
changed = true
walletData
wallet
} else {
it
}
}
return if (changed) updatedWallets else walletsData + walletData
return if (changed) updatedWallets else wallets + wallet
}
fun updateWalletData(walletData: WalletData?): WalletState {
if (walletData == null) return this
return updateWalletsData(listOf(walletData))
}
fun updateWalletsData(
walletsData: List<WalletData>
): WalletState {
val walletStores = walletsData
.map { BlockchainNetwork(it.currency.blockchain, it.currency.derivationPath, emptyList()) }
.distinct().map { getWalletStore(it) }.mapNotNull { it?.updateWallets(walletsData) }
return updateWalletStores(walletStores)
}
fun updateWalletStore(walletStore: WalletStore?): WalletState {
return copy(wallets = replaceWalletInWallets(walletStore))
}
fun updateWalletStores(walletStores: List<WalletStore>): WalletState {
val walletStores = walletStores.toMutableList()
val updatedWallets = wallets.map { oldWalletStore ->
val walletStore = walletStores.find { it.blockchainNetwork == oldWalletStore.blockchainNetwork }
if (walletStore != null) {
walletStores.remove(walletStore)
walletStore
} else {
oldWalletStore
}
}
return copy(wallets = updatedWallets + walletStores)
}
fun removeWallet(walletData: WalletData?): WalletState {
if (walletData == null) return this
return if (walletData.currency is Currency.Blockchain) {
val walletStores = wallets.filterNot {
it.blockchainNetwork.blockchain == walletData.currency.blockchain
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
}
copy(wallets = walletStores)
} else {
val walletStore = getWalletStore(walletData.currency)
val walletDataList = walletStore?.walletsData
?.filterNot { it.currency == walletData.currency }
?: emptyList()
val updatedWalletStore = walletStore?.copy(walletsData = walletDataList)
updateWalletStore(updatedWalletStore)
}
}
fun replaceSomeWallets(newWallets: List<WalletData>): List<WalletData> {
@ -165,20 +271,26 @@ data class WalletState(
exchangeManager: CurrencyExchangeManager?,
walletData: WalletData
): WalletData {
return walletData.copy(tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData))
return walletData.copy(
tradeCryptoState = TradeCryptoState.from(
exchangeManager,
walletData
)
)
}
fun updateTradeCryptoState(
exchangeManager: CurrencyExchangeManager?,
walletDataList: List<WalletData>
): List<WalletData> {
return walletDataList.map { it.copy(tradeCryptoState = TradeCryptoState.from(exchangeManager, it)) }
}
fun addWalletManagers(newWalletManagers: List<WalletManager>): WalletState {
val updatedWalletManagers = this.walletManagers +
newWalletManagers.filterNot { this.blockchains.contains(it.wallet.blockchain) }
return copy(walletManagers = updatedWalletManagers)
return walletDataList.map {
it.copy(
tradeCryptoState = TradeCryptoState.from(
exchangeManager,
it
)
)
}
}
}
@ -224,7 +336,8 @@ data class Artwork(
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
const val TEMP_CARDANO = "https://verify.tangem.com/card/artwork?artworkId=card_ru039&CID=CB19000000040976&publicKey=0416E29423A6CC77CD07CBA52873E8F6F894B1AFB18EB3688ACC2C8D8E5AC84B80B0BA1B17B85E578E47044CE96BCFF3FB4499FA4941CAD3C1EF300A492B5B9659"
const val TEMP_CARDANO =
"https://verify.tangem.com/card/artwork?artworkId=card_ru039&CID=CB19000000040976&publicKey=0416E29423A6CC77CD07CBA52873E8F6F894B1AFB18EB3688ACC2C8D8E5AC84B80B0BA1B17B85E578E47044CE96BCFF3FB4499FA4941CAD3C1EF300A492B5B9659"
}
}
@ -233,7 +346,10 @@ data class TradeCryptoState(
val buyingAllowed: Boolean = false,
) {
companion object {
fun from(exchangeManager: CurrencyExchangeManager?, walletData: WalletData): TradeCryptoState {
fun from(
exchangeManager: CurrencyExchangeManager?,
walletData: WalletData
): TradeCryptoState {
val status = exchangeManager ?: return walletData.tradeCryptoState
val currency = walletData.currency
@ -267,7 +383,9 @@ data class WalletData(
fun shouldEnableTokenSendButton(): Boolean = !blockchainAmountIsEmpty() || !tokenAmountIsEmpty()
private fun blockchainAmountIsEmpty(): Boolean = currencyData.blockchainAmount?.isZero() ?: false
private fun blockchainAmountIsEmpty(): Boolean =
currencyData.blockchainAmount?.isZero() ?: false
private fun tokenAmountIsEmpty(): Boolean = currencyData.amount?.isZero() == true
}
@ -280,17 +398,83 @@ sealed interface Currency {
val blockchain: com.tangem.blockchain.common.Blockchain
val currencySymbol: CryptoCurrencyName
val derivationPath: String?
data class Token(
val token: com.tangem.blockchain.common.Token
val token: com.tangem.blockchain.common.Token,
override val blockchain: com.tangem.blockchain.common.Blockchain,
override val derivationPath: String?
) : Currency {
override val blockchain = token.blockchain
override val currencySymbol: CryptoCurrencyName = token.symbol
override val currencySymbol = token.symbol
}
data class Blockchain(
override val blockchain: com.tangem.blockchain.common.Blockchain
override val blockchain: com.tangem.blockchain.common.Blockchain,
override val derivationPath: String?
) : Currency {
override val currencySymbol: CryptoCurrencyName = blockchain.currency
}
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
if (derivationPath == null || derivationStyle == null) return false
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
}
companion object {
fun fromBlockchainNetwork(
blockchainNetwork: BlockchainNetwork,
token: com.tangem.blockchain.common.Token? = null
): Currency {
return if (token != null) {
Currency.Token(
token = token,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
)
} else {
Currency.Blockchain(
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
)
}
}
}
}
data class WalletStore(
val walletManager: WalletManager?,
val blockchainNetwork: BlockchainNetwork,
val walletsData: List<WalletData>
) {
fun updateWallets(walletDataList: List<WalletData>): WalletStore {
val relevantWalletDataList = walletDataList.filter {
it.currency.blockchain == blockchainNetwork.blockchain &&
it.currency.derivationPath == blockchainNetwork.derivationPath
}.toMutableList()
val updatedWalletDataList = walletsData.map { walletData ->
val matchingWalletData = relevantWalletDataList.find { it.currency == walletData.currency }
if (matchingWalletData != null) relevantWalletDataList.remove(matchingWalletData)
matchingWalletData ?: walletData
}
return copy(walletsData = updatedWalletDataList + relevantWalletDataList)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WalletStore
if (walletManager != other.walletManager) return false
if (blockchainNetwork != other.blockchainNetwork) return false
return true
}
override fun hashCode(): Int {
var result = walletManager?.hashCode() ?: 0
result = 31 * result + blockchainNetwork.hashCode()
return result
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.Currency
@ -31,11 +32,8 @@ class MultiWalletMiddleware {
val tapWalletManager = globalState.tapWalletManager
when (action) {
is WalletAction.MultiWallet.AddWalletManagers -> {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(action.walletManagers)
if (globalState.scanResponse?.isDemoCard() == true) {
addDummyBalances(action.walletManagers)
}
is WalletAction.MultiWallet.AddBlockchains -> {
handleAddingWalletManagers(globalState, action.walletManagers)
}
is WalletAction.MultiWallet.SelectWallet -> {
if (action.walletData != null) {
@ -43,33 +41,39 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.AddToken -> {
globalState.scanResponse?.card?.cardId?.let {
currenciesRepository.saveAddedToken(it, action.token)
}
addTokens(listOf(action.token), walletState, globalState)
addTokens(listOf(action.token), action.blockchain, walletState, globalState)
}
is WalletAction.MultiWallet.AddTokens -> {
addTokens(action.tokens, walletState, globalState)
addTokens(action.tokens, action.blockchain, walletState, globalState)
}
is WalletAction.MultiWallet.AddBlockchain -> {
globalState.scanResponse?.let {
currenciesRepository.saveAddedBlockchain(it.card.cardId, action.blockchain)
if (walletState?.blockchains?.contains(action.blockchain) != true) {
tapWalletManager.walletManagerFactory
.makeWalletManagerForApp(it, action.blockchain)?.let { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
}
}
action.walletManager?.let {
handleAddingWalletManagers(globalState, listOf(action.walletManager))
}
store.dispatch(WalletAction.LoadFiatRate(
currencyList = listOf(Currency.Blockchain(action.blockchain)))
globalState.scanResponse?.let {
currenciesRepository.saveUpdatedCurrency(
cardId = it.card.cardId,
blockchainNetwork = action.blockchain
)
}
store.dispatch(
WalletAction.LoadFiatRate(
currencyList = listOf(
Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
)
)
)
)
store.dispatch(WalletAction.LoadWallet(action.blockchain)
store.dispatch(
WalletAction.LoadWallet(action.blockchain)
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
globalState.scanResponse?.card?.cardId?.let {
currenciesRepository.saveCardCurrencies(it, action.cardCurrencies)
currenciesRepository.saveCurrencies(it, action.blockchainNetworks)
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
@ -77,13 +81,27 @@ class MultiWalletMiddleware {
when (val currency = action.walletData.currency) {
is Currency.Blockchain -> {
cardId?.let {
currenciesRepository.removeBlockchain(it, currency.blockchain)
currenciesRepository.removeBlockchain(
cardId = it,
blockchainNetwork = BlockchainNetwork(
currency.blockchain, currency.derivationPath,
emptyList()
)
)
}
}
is Currency.Token -> {
walletState?.getWalletManager(currency.token)
?.removeToken(currency.token)
cardId?.let { currenciesRepository.removeToken(it, currency.token) }
val walletManager = walletState?.getWalletManager(currency.token)
if (walletManager != null) {
walletManager.removeToken(currency.token)
cardId?.let {
currenciesRepository.removeToken(
cardId = it,
token = currency.token,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
}
}
}
@ -94,8 +112,12 @@ class MultiWalletMiddleware {
val cardFirmware = scanResponse.card.firmwareVersion
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
.filterNot { walletState?.blockchains?.contains(it) == true }
.map { BlockchainNetwork(it, null, emptyList()) }
val walletManagers =
tapWalletManager.walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains)
tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
scanResponse,
blockchains
)
scope.launch {
walletManagers.map { walletManager ->
@ -105,18 +127,19 @@ class MultiWalletMiddleware {
val coinAmount = wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
scope.launch(Dispatchers.Main) {
if (walletState?.getWalletData(wallet.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddWalletManagers(
listOfNotNull(walletManager)
)
)
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
if (walletState?.getWalletData(blockchainNetwork) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
wallet.blockchain
blockchainNetwork, walletManager
)
)
store.dispatch(
WalletAction.LoadWallet.Success(
wallet = wallet,
blockchain = blockchainNetwork
)
)
store.dispatch(WalletAction.LoadWallet.Success(wallet))
}
}
}
@ -130,30 +153,42 @@ class MultiWalletMiddleware {
val walletFactory = tapWalletManager.walletManagerFactory
val card = scanResponse.card
val walletManager = walletState?.getWalletManager(Blockchain.Ethereum)
?: walletFactory.makeWalletManagerForApp(scanResponse, Blockchain.Ethereum)
val walletManager = walletState?.getWalletManager(
Currency.Blockchain(Blockchain.Ethereum, null)
)
?: walletFactory.makeWalletManagerForApp(
scanResponse,
Currency.Blockchain(Blockchain.Ethereum, null)
)
val tokenFinder = walletManager as? TokenFinder ?: return
scope.launch {
val result = tokenFinder.findTokens()
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
if (result.data.isNotEmpty()) {
currenciesRepository.saveAddedTokens(card.cardId, result.data)
val blockchainNetwork = BlockchainNetwork(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
walletManager.cardTokens.toList()
)
currenciesRepository.saveUpdatedCurrency(
card.cardId,
blockchainNetwork
)
store.dispatch(
WalletAction.MultiWallet.AddWalletManagers(
WalletAction.MultiWallet.AddBlockchain(
blockchainNetwork,
walletManager
)
)
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
walletManager.wallet.blockchain
)
)
store.dispatch(
WalletAction.MultiWallet.AddTokens(
walletManager.cardTokens.toList()
walletManager.cardTokens.toList(),
blockchainNetwork
)
)
}
@ -173,38 +208,60 @@ class MultiWalletMiddleware {
}
}
private fun addTokens(tokens: List<Token>, walletState: WalletState?, globalState: GlobalState?) {
private fun handleAddingWalletManagers(
globalState: GlobalState,
walletManagers: List<WalletManager>
) {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(walletManagers)
if (globalState.scanResponse?.isDemoCard() == true) {
addDummyBalances(walletManagers)
}
}
private fun addTokens(
tokens: List<Token>, blockchainNetwork: BlockchainNetwork,
walletState: WalletState?, globalState: GlobalState?
) {
val scanResponse = globalState?.scanResponse ?: return
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val groupedTokens = tokens.groupBy { it.blockchain }
val walletManagers = groupedTokens.mapNotNull { entry ->
val blockchain = entry.key
val tokensList = entry.value
val walletManager = walletState?.getWalletManager(blockchain)
?: wmFactory.makeWalletManagerForApp(scanResponse, blockchain)?.also {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(it))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain))
}
store.dispatch(WalletAction.LoadFiatRate(currencyList = tokensList.map { Currency.Token(it) }))
walletManager?.apply { addTokens(tokensList) }
}
val walletManager = walletState?.getWalletManager(blockchainNetwork)
?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, it))
} ?: return
store.dispatch(WalletAction.LoadFiatRate(currencyList = tokens.map { token ->
Currency.Token(
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
)
}))
walletManager.addTokens(tokens)
currenciesRepository.saveUpdatedCurrency(
cardId = scanResponse.card.cardId,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
)
scope.launch {
walletManagers.forEach { walletManager ->
when (val result = walletManager.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token -> wallet.getTokenAmount(token)?.let { Pair(token, it) } }
.forEach {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.MultiWallet.TokenLoaded(it.second, it.first))
}
when (val result = walletManager.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token ->
wallet.getTokenAmount(token)?.let { Pair(token, it) }
}
.forEach {
withContext(Dispatchers.Main) {
store.dispatch(
WalletAction.MultiWallet.TokenLoaded(
it.second,
it.first,
blockchainNetwork
)
)
}
}
is com.tangem.common.services.Result.Failure -> {}
}
}
is com.tangem.common.services.Result.Failure -> {}
}
}
}

View file

@ -5,8 +5,6 @@ import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.isZero
@ -25,10 +23,7 @@ import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.scope
import com.tangem.tap.store
@ -63,7 +58,11 @@ class WalletMiddleware {
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(
action,
walletState,
globalState
)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
@ -79,9 +78,19 @@ class WalletMiddleware {
is WalletAction.LoadWallet.Success -> {
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState.getWalletData(action.wallet.blockchain) == null) {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(action.wallet.blockchain))
store.dispatch(WalletAction.LoadWallet.Success(action.wallet))
if (walletState.getWalletData(action.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
)
)
store.dispatch(
WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain
)
)
}
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
@ -121,7 +130,8 @@ class WalletMiddleware {
)
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse = globalState.scanResponse?.copy(card = result.data)
val scanNoteResponse =
globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) }
}
is CompletionResult.Failure -> {
@ -197,8 +207,10 @@ class WalletMiddleware {
}
}
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData = walletState.getWalletData(walletState.selectedCurrency) ?: return
val selectedAddressData = selectedWalletData.walletAddresses?.selectedAddress ?: return
val selectedWalletData =
walletState.getWalletData(walletState.selectedCurrency) ?: return
val selectedAddressData =
selectedWalletData.walletAddresses?.selectedAddress ?: return
val currency = selectedWalletData.currency
store.dispatchDialogShow(AppDialog.AddressInfoDialog(currency, selectedAddressData))
@ -209,28 +221,38 @@ class WalletMiddleware {
private fun prepareSendAction(amount: Amount?, state: WalletState?): Action {
val selectedWalletData = state?.getSelectedWalletData()
val currency = selectedWalletData?.currency
val walletManager = state?.getWalletManager(currency)
val wallet = walletManager?.wallet
val walletStore = state?.getWalletStore(currency)
return if (amount != null) {
if (amount.type is AmountType.Token) {
prepareSendActionForToken(amount, state, selectedWalletData, wallet, walletManager)
prepareSendActionForToken(amount, state, selectedWalletData, walletStore)
} else {
PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletManager)
PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletStore?.walletManager)
}
} else {
val amounts = wallet?.amounts?.toSendableAmounts()
val amounts = walletStore?.walletManager?.wallet?.amounts?.toSendableAmounts()
if (currency != null && state.isMultiwalletAllowed) {
when (currency) {
is Currency.Blockchain -> {
val amountToSend = amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
PrepareSendScreen(amountToSend, selectedWalletData.fiatRate, walletManager)
val amountToSend =
amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData.fiatRate,
walletManager = walletStore.walletManager
)
}
is Currency.Token -> {
val amountToSend = amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.Send.ChooseCurrency(amounts)
prepareSendActionForToken(amountToSend, state, selectedWalletData, wallet, walletManager)
val amountToSend =
amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.Send.ChooseCurrency(amounts)
prepareSendActionForToken(
amount = amountToSend,
state = state,
selectedWalletData = selectedWalletData,
walletStore = walletStore
)
}
}
} else {
@ -238,24 +260,36 @@ class WalletMiddleware {
WalletAction.Send.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate, walletManager)
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData?.fiatRate,
walletManager = walletStore?.walletManager
)
}
}
}
}
private fun prepareSendActionForToken(
amount: Amount, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet?,
walletManager: WalletManager?
amount: Amount,
state: WalletState?,
selectedWalletData: WalletData?,
walletStore: WalletStore?
): PrepareSendScreen {
val coinRate = state?.getWalletData(wallet?.blockchain)?.fiatRate
val coinRate = state?.getWalletData(walletStore?.blockchainNetwork)?.fiatRate
val tokenRate = if (state?.isMultiwalletAllowed == true) {
selectedWalletData?.fiatRate
} else {
selectedWalletData?.currencyData?.token?.fiatRate
}
val coinAmount = walletStore?.walletManager?.wallet?.amounts?.get(AmountType.Coin)
return PrepareSendScreen(
wallet?.amounts?.get(AmountType.Coin), coinRate, walletManager,
amount, tokenRate)
coinAmount = coinAmount,
coinRate = coinRate,
walletManager = walletStore?.walletManager,
tokenAmount = amount,
tokenRate = tokenRate
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
@ -17,66 +18,85 @@ import com.tangem.tap.store
class MultiWalletReducer {
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.AddWalletManagers -> {
state.addWalletManagers(action.walletManagers)
}
is WalletAction.MultiWallet.AddBlockchains -> {
val walletsData = action.blockchains.map { blockchain ->
val wallet = state.getWalletManager(blockchain)?.wallet
val wallets: List<WalletStore> = action.blockchains.map { blockchain ->
val walletManager = action.walletManagers.first {
it.wallet.blockchain == blockchain.blockchain &&
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
}
val wallet = walletManager.wallet
val cardToken = if (!state.isMultiwalletAllowed) {
wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
wallet.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
} else {
null
}
WalletData(
val walletData = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
blockchain.fullName,
currencySymbol = blockchain.currency,
blockchain.blockchain.fullName,
currencySymbol = blockchain.blockchain.currency,
token = cardToken
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(blockchain),
currency = Currency.Blockchain(
blockchain.blockchain,
blockchain.derivationPath
),
)
WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchain,
walletsData = listOf(walletData)
)
}
val selectedCurrency = if (!state.isMultiwalletAllowed) {
walletsData[0].currency
wallets[0].walletsData[0].currency
} else {
state.selectedCurrency
}
state.copy(
walletsData = walletsData,
wallets = wallets,
selectedCurrency = selectedCurrency
)
}
is WalletAction.MultiWallet.AddBlockchain -> {
val wallet = state.getWalletManager(action.blockchain)?.wallet
val walletManager = action.walletManager ?: state.getWalletManager(action.blockchain)
val wallet = walletManager?.wallet
val walletData = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
action.blockchain.fullName,
currencySymbol = action.blockchain.currency,
action.blockchain.blockchain.fullName,
currencySymbol = action.blockchain.blockchain.currency,
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(action.blockchain),
currency = Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
),
)
val newState = state.copy(walletsData = state.replaceWalletInWallets(walletData))
val walletStore = WalletStore(
walletManager = walletManager,
blockchainNetwork = action.blockchain,
walletsData = listOf(walletData)
)
val newState = state.updateWalletStore(walletStore)
if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) {
OnWalletLoadedReducer().reduce(wallet, newState)
OnWalletLoadedReducer().reduce(wallet, action.blockchain, newState)
} else {
newState
}
}
is WalletAction.MultiWallet.AddTokens -> {
val wallets = action.tokens.mapNotNull { token -> token.toWallet(state) }
state.copy(walletsData = state.replaceSomeWallets(wallets))
addTokens(action.tokens, action.blockchain, state)
}
is WalletAction.MultiWallet.AddToken -> {
val walletData = action.token.toWallet(state) ?: return state
state.copy(walletsData = state.replaceWalletInWallets(walletData))
addTokens(listOf(action.token), action.blockchain, state)
}
is WalletAction.MultiWallet.TokenLoaded -> {
val pendingTransactions = state.getWalletManager(action.token)
@ -84,7 +104,8 @@ class MultiWalletReducer {
wallet.recentTransactions.toPendingTransactions(wallet.address)
} ?: emptyList()
val sendButtonEnabled = action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val sendButtonEnabled =
action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val tokenPendingTransactions = pendingTransactions
.filter { it.currency == action.amount.currencySymbol }
val tokenBalanceStatus = when {
@ -107,10 +128,13 @@ class MultiWalletReducer {
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
currency = Currency.Token(action.token)
currency = Currency.Token(
token = action.token,
blockchain = action.blockchain.blockchain,
derivationPath = action.blockchain.derivationPath
)
)
val wallets = state.replaceWalletInWallets(newTokenWalletData)
state.copy(walletsData = wallets)
state.updateWalletData(newTokenWalletData)
}
is WalletAction.MultiWallet.SetIsMultiwalletAllowed ->
state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed)
@ -119,21 +143,7 @@ class MultiWalletReducer {
state.copy(selectedCurrency = action.walletData?.currency)
is WalletAction.MultiWallet.RemoveWallet -> {
val wallets = state.walletsData.filterNot {
it.currency == action.walletData.currency
}
if (action.walletData.currency is Currency.Blockchain) {
state.copy(
walletsData = wallets,
walletManagers = state.walletManagers.filterNot {
it.wallet.blockchain == action.walletData.currency.blockchain
}
)
} else {
state.copy(walletsData = wallets)
}
state.removeWallet(action.walletData)
}
is WalletAction.MultiWallet.SetPrimaryBlockchain ->
state.copy(primaryBlockchain = action.blockchain)
@ -147,7 +157,14 @@ class MultiWalletReducer {
}
}
fun Token.toWallet(state: WalletState): WalletData? {
private fun addTokens(
tokens: List<Token>, blockchain: BlockchainNetwork, state: WalletState
): WalletState {
val wallets = tokens.mapNotNull { token -> token.toWallet(state, blockchain) }
return state.updateWalletsData(wallets)
}
fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? {
if (!state.isMultiwalletAllowed) return null
if (state.currencies.any { it is Currency.Token && it.token == this }) {
return null
@ -164,6 +181,6 @@ fun Token.toWallet(state: WalletState): WalletData? {
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Token(this),
currency = Currency.fromBlockchainNetwork(blockchain, this)
)
}

View file

@ -8,6 +8,7 @@ import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
@ -19,25 +20,30 @@ import java.math.RoundingMode
class OnWalletLoadedReducer {
fun reduce(wallet: Wallet, walletState: WalletState): WalletState {
fun reduce(wallet: Wallet, blockchainNetwork: BlockchainNetwork, walletState: WalletState): WalletState {
return if (!walletState.isMultiwalletAllowed) {
onSingleWalletLoaded(wallet, walletState)
} else {
onMultiWalletLoaded(wallet, walletState)
onMultiWalletLoaded(wallet, blockchainNetwork, walletState)
}
}
private fun onMultiWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
private fun onMultiWalletLoaded(
wallet: Wallet,
blockchainNetwork: BlockchainNetwork,
walletState: WalletState
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val exchangeManager = store.state.globalState.currencyExchangeManager
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
if (walletState.getWalletData(wallet.blockchain) == null) {
if (walletState.getWalletData(blockchainNetwork) == null) {
return walletState
}
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
wallet.blockchain.currency
)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
@ -48,7 +54,7 @@ class OnWalletLoadedReducer {
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.getWalletData(wallet.blockchain)
val walletData = walletState.getWalletData(blockchainNetwork)
val fiatAmount = walletData?.fiatRate?.let { coinAmountValue?.toFiatValue(it) }
val newWalletData = walletData?.copy(
@ -63,29 +69,34 @@ class OnWalletLoadedReducer {
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(coinSendButton),
currency = Currency.Blockchain(wallet.blockchain),
currency = Currency.fromBlockchainNetwork(blockchainNetwork),
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData),
)
val tokens = wallet.getTokens().mapNotNull { token ->
val tokenWalletData = walletState.getWalletData(token)
val tokenPendingTransactions = pendingTransactions.filter { it.currency == token.symbol }
val tokenPendingTransactions =
pendingTransactions.filter { it.currency == token.symbol }
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenAmountValue = wallet.getTokenAmount(token)?.value
val tokenFiatAmount = tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate) }
val tokenFiatAmount =
tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate) }
val tokenSendButton = newWalletData?.shouldEnableTokenSendButton() == true
&& tokenPendingTransactions.isEmpty()
&& tokenPendingTransactions.isEmpty()
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
blockchainAmount = coinAmountValue,
amount = tokenAmountValue,
amountFormatted = tokenAmountValue?.toFormattedCurrencyString(token.decimals, token.symbol),
amountFormatted = tokenAmountValue?.toFormattedCurrencyString(
token.decimals,
token.symbol
),
fiatAmount = tokenFiatAmount,
fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrencySymbol)
),
@ -102,8 +113,9 @@ class OnWalletLoadedReducer {
} else {
ProgressState.Done
}
return walletState.copy(
state = state, walletsData = wallets, error = null
val newState = walletState.updateWalletsData(wallets)
return newState.copy(
state = state, error = null
)
}
@ -118,7 +130,8 @@ class OnWalletLoadedReducer {
val tokenAmount = wallet.getTokenAmount(token)
if (tokenAmount != null) {
val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate
val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) }
val tokenFiatAmount =
tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) }
TokenData(
tokenAmount.value?.toFormattedCurrencyString(
token.decimals, token.symbol
@ -134,7 +147,8 @@ class OnWalletLoadedReducer {
val amount = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
wallet.blockchain.currency
)
val fiatRate = walletState.primaryWallet?.fiatRate
val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
@ -161,9 +175,11 @@ class OnWalletLoadedReducer {
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet),
)
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
return walletState.copy(
state = ProgressState.Done, walletsData = wallets, error = null
val wallets = listOfNotNull(walletData)
val updatedStore = walletState.getWalletStore(walletData?.currency)?.updateWallets(wallets)
return walletState.updateWalletStore(updatedStore).copy(
state = ProgressState.Done, error = null
)
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.tap.common.redux.global.FiatCurrencyName
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.redux.*
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
@ -46,11 +47,21 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.EmptyWallet -> {
newState = newState.copy(
state = ProgressState.Done,
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
currency = Currency.Blockchain(Blockchain.Unknown)
wallets = listOf(
WalletStore(
walletManager = null,
blockchainNetwork = BlockchainNetwork(
Blockchain.Unknown,
null,
emptyList()
),
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
currency = Currency.Blockchain(Blockchain.Unknown, null)
)
)
)
)
)
@ -58,27 +69,43 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallets = newState.walletsData
val wallets = newState.wallets
.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable
)
walletsData = it.walletsData.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable
)
)
}
)
}
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
walletsData = wallets
wallets = wallets
)
}
is TapError.UnknownBlockchain -> {
newState = newState.copy(
state = ProgressState.Done,
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
currency = Currency.Blockchain(Blockchain.Unknown)
wallets = listOf(
WalletStore(
walletManager = null,
blockchainNetwork = BlockchainNetwork(
Blockchain.Unknown,
null,
emptyList()
),
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
currency = Currency.Blockchain(Blockchain.Unknown, null)
)
)
)
)
)
@ -94,26 +121,37 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
is WalletAction.LoadWallet -> {
if (action.blockchain == null) {
val wallets = newState.walletsData.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = BalanceStatus.Loading,
currency = wallet.currencyData.currency,
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
val wallets = newState.wallets.map {
it.copy(
walletsData = it.walletsData.map { walletData ->
walletData.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Loading,
currency = walletData.currencyData.currency,
currencySymbol = walletData.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState.from(
exchangeManager,
walletData
)
)
}
)
}
newState = newState.copy(
state = ProgressState.Loading,
walletsData = wallets,
wallets = wallets,
)
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
val blockchain = walletManager.wallet.blockchain
val currencies = listOf(Currency.Blockchain(blockchain)) +
walletManager.cardTokens.map { Currency.Token(it) }
val currencies = listOf(Currency.fromBlockchainNetwork(action.blockchain)) +
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(
action.blockchain,
it
)
}
val newWallets = newState.walletsData.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
@ -126,38 +164,52 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
)
}
val wallets = newState.replaceSomeWallets(newWallets)
newState = newState.copy(walletsData = newState.updateTradeCryptoState(exchangeManager, wallets))
val wallets = newState.updateTradeCryptoState(exchangeManager, newState.replaceSomeWallets(newWallets))
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets)
newState = newState.updateWalletStore(walletStore)
}
}
is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(action.wallet, newState)
is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(
wallet = action.wallet,
blockchainNetwork = action.blockchain,
walletState = newState
)
is WalletAction.LoadWallet.NoAccount -> {
val walletData = newState.getWalletData(action.wallet.blockchain)?.copy(
val walletData = newState.getWalletData(action.blockchain)?.copy(
currencyData = BalanceWidgetData(
BalanceStatus.NoAccount, action.wallet.blockchain.fullName,
currencySymbol = action.wallet.blockchain.currency,
amountToCreateAccount = action.amountToCreateAccount
)
)
val wallets = newState.replaceWalletInWallets(walletData)
var updatedWalletStore = newState.getWalletStore(action.blockchain)
?.updateWallets(listOfNotNull(walletData))
val progressState =
if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
if (updatedWalletStore?.walletsData?.any { it.currencyData.status == BalanceStatus.Loading } == true) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = progressState,
walletsData = newState.updateTradeCryptoState(exchangeManager, wallets)
)
updatedWalletStore =
updatedWalletStore?.updateWallets(
newState.updateTradeCryptoState(exchangeManager, updatedWalletStore.walletsData)
)
newState = newState.updateWalletStore(updatedWalletStore)
.copy(
state = progressState
)
}
is WalletAction.LoadWallet.Failure -> {
val message = if (newState.error == ErrorType.NoInternetConnection) {
null
} else {
action.errorMessage
}
val walletData = newState.getWalletData(action.wallet.blockchain)
val walletStore = newState.getWalletStore(action.wallet)
val walletData = walletStore?.walletsData?.first { it.currency is Currency.Blockchain }
val newWalletData = walletData?.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Unreachable,
@ -173,17 +225,23 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
val wallets = newState.replaceSomeWallets(listOfNotNull(newWalletData) + tokenWallets)
val updatedWallets =
newState.updateTradeCryptoState(
exchangeManager,
walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData
)
newState = newState.updateWalletsData(updatedWallets)
val progressState =
if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
if (newState.walletsData.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = progressState,
walletsData = newState.updateTradeCryptoState(exchangeManager, wallets)
)
}
is WalletAction.SetArtworkId -> {
@ -230,25 +288,21 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
val wallets = newState.replaceWalletInWallets(
selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
)
newState = newState.updateWalletData(selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
)
)
newState = newState.copy(walletsData = wallets)
))
}
is WalletAction.SetWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData == null) {
newState
} else {
walletData = walletData.copy(warningRent = WalletRent(action.minRent, action.rentExempt))
newState = newState.copy(
walletsData = newState.replaceSomeWallets(listOf(walletData))
)
walletData =
walletData.copy(warningRent = WalletRent(action.minRent, action.rentExempt))
newState = newState.updateWalletsData(listOf(walletData))
}
}
}
@ -322,8 +376,11 @@ private fun setMultiWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: Currency,
appCurrency: FiatCurrencyName, state: WalletState
): WalletState {
val walletStore = state.getWalletStore(currency) ?: return state
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return state
val wallet = state.getWalletManager(currency)?.wallet
val fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
@ -338,14 +395,14 @@ private fun setMultiWalletFiatRate(
),
fiatRate = rate, fiatRateString = rateFormatted
)
return state.copy(walletsData = state.replaceWalletInWallets(newWalletData))
return state.updateWalletData(newWalletData)
}
private fun setSingeWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: Currency,
appCurrency: FiatCurrencyName, state: WalletState
): WalletState {
val wallet = state.walletManagers[0].wallet
val wallet = state.primaryWalletManager?.wallet ?: return state
val token = wallet.getFirstToken()
if (currency == state.primaryWallet?.currency) {
@ -356,7 +413,7 @@ private fun setSingeWalletFiatRate(
fiatRate = rate,
fiatRateString = rateFormatted
)
return state.copy(walletsData = listOf(walletData))
return state.updateWalletData(walletData)
} else if (currency is Currency.Token && currency.token == token) {
val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency)
val tokenData = state.primaryWallet?.currencyData?.token?.copy(
@ -371,7 +428,7 @@ private fun setSingeWalletFiatRate(
)
)
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
return state.copy(walletsData = wallets)
return state.updateWalletsData(wallets)
}
return state
}

View file

@ -18,6 +18,7 @@ import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.onboarding.getQRReceiveMessage
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
@ -114,7 +115,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(
WalletAction.LoadWallet(
blockchain = selectedWallet.currency.blockchain
blockchain = BlockchainNetwork(selectedWallet.currency.blockchain, selectedWallet.currency.derivationPath, emptyList())
)
)
}
@ -188,7 +189,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
binding.rvPendingTransaction?.show(pendingTransactions.isNotEmpty())
binding.rvPendingTransaction.show(pendingTransactions.isNotEmpty())
}
private fun setupAddressCard(state: WalletData) = with(binding.lWalletDetails) {
@ -212,7 +213,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
chipGroupAddressType.hide()
}
tvAddress.text = state.walletAddresses.selectedAddress.address
tvExplore?.setOnClickListener {
tvExplore.setOnClickListener {
store.dispatch(
WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,

View file

@ -11,6 +11,7 @@ import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.TapWorkarounds.derivationStyle
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
@ -26,7 +27,11 @@ class WalletAdapter
return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0
}
fun submitList(list: List<WalletData>, primaryBlockchain: Blockchain?, primaryToken: Token? = null) {
fun submitList(
list: List<WalletData>,
primaryBlockchain: Blockchain?,
primaryToken: Token? = null
) {
// We used this method to sort the list of currencies. Sorting is disabled for now.
super.submitList(list)
}
@ -44,21 +49,22 @@ class WalletAdapter
object DiffUtilCallback : DiffUtil.ItemCallback<WalletData>() {
override fun areContentsTheSame(
oldItem: WalletData, newItem: WalletData
oldItem: WalletData, newItem: WalletData
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: WalletData, newItem: WalletData
oldItem: WalletData, newItem: WalletData
) = oldItem == newItem
}
class WalletsViewHolder(val binding: ItemCurrencyWalletBinding) :
RecyclerView.ViewHolder(binding.root) {
RecyclerView.ViewHolder(binding.root) {
fun bind(wallet: WalletData) = with(binding) {
tvCurrency.text = wallet.currencyData.currency
tvAmount.text = wallet.currencyData.amountFormatted?.takeWhile { !it.isWhitespace() }
tvCurrencySymbol.text = wallet.currencyData.amountFormatted?.takeLastWhile { !it.isWhitespace() }
tvCurrencySymbol.text =
wallet.currencyData.amountFormatted?.takeLastWhile { !it.isWhitespace() }
tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted
tvExchangeRate.text = wallet.fiatRateString
cardWallet.setOnClickListener {
@ -67,6 +73,11 @@ class WalletAdapter
val blockchain = wallet.currency.blockchain
val token = (wallet.currency as? Currency.Token)?.token
val isCustom =
wallet.currency.isCustomCurrency(store.state.globalState.scanResponse?.card?.derivationStyle)
tvExchangeRate.show(!isCustom)
tvCustomCurrency.show(isCustom)
Picasso.get().loadCurrenciesIcon(
imageView = ivCurrency,
textView = tvTokenLetter,

View file

@ -8,6 +8,9 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.StateDialog
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.TapWorkarounds.derivationStyle
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletDialog
@ -97,8 +100,20 @@ class MultiWalletView : WalletView {
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
binding.btnAddToken.setOnClickListener {
store.dispatch(TokensAction.LoadCurrencies)
store.dispatch(TokensAction.SetAddedCurrencies(state.walletsData))
val card = store.state.globalState.scanResponse!!.card
store.dispatch(TokensAction.LoadCurrencies(
supportedBlockchains = currenciesRepository.getBlockchains(
card.firmwareVersion,
card.isTestCard
)))
store.dispatch(TokensAction.AllowToAddTokens(true))
store.dispatch(TokensAction.SetAddedCurrencies(
wallets = state.walletsData,
derivationStyle = card.derivationStyle
))
store.dispatch(TokensAction.SetNonRemovableCurrencies(
state.walletsData.filterNot { state.canBeRemoved(it) })
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="10dp"
android:viewportHeight="10" android:viewportWidth="17"
android:width="17dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#CCCCCC" android:fillType="evenOdd" android:pathData="M0.2689,0.95C0.6458,0.5463 1.2786,0.5244 1.6823,0.9013L8.5,7.2644L15.3177,0.9013C15.7214,0.5244 16.3542,0.5463 16.7311,0.95C17.1079,1.3538 17.0861,1.9865 16.6823,2.3634L8.5,10.0002L0.3177,2.3634C-0.0861,1.9865 -0.1079,1.3538 0.2689,0.95Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="10dp"
android:viewportHeight="10" android:viewportWidth="17"
android:width="17dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#CCCCCC" android:fillType="evenOdd" android:pathData="M0.2689,9.6823C0.6458,10.0861 1.2786,10.1079 1.6823,9.731L8.5,3.3679L15.3177,9.731C15.7214,10.1079 16.3542,10.0861 16.7311,9.6823C17.1079,9.2786 17.0861,8.6458 16.6823,8.2689L8.5,0.6321L0.3177,8.2689C-0.0861,8.6458 -0.1079,9.2786 0.2689,9.6823Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#F1F1F1" android:fillType="evenOdd" android:pathData="M16,8C16,12.4183 12.4183,16 8,16C3.5817,16 0,12.4183 0,8C0,3.5817 3.5817,0 8,0C12.4183,0 16,3.5817 16,8ZM5.733,11.1836H4.1804C3.8542,11.1836 3.6931,11.1836 3.5948,11.1207C3.4887,11.0519 3.4238,10.9379 3.416,10.8121C3.4101,10.6962 3.4906,10.5547 3.6518,10.2717L7.4853,3.5146C7.6484,3.2277 7.7309,3.0842 7.8351,3.0312C7.9471,2.9742 8.0808,2.9742 8.1928,3.0312C8.297,3.0842 8.3795,3.2277 8.5426,3.5146L9.3307,4.8903L9.3347,4.8973C9.5109,5.2052 9.6002,5.3613 9.6392,5.5251C9.6825,5.7039 9.6825,5.8926 9.6392,6.0715C9.5999,6.2365 9.5115,6.3938 9.3327,6.7063L7.319,10.2658L7.3138,10.2749C7.1365,10.5853 7.0466,10.7426 6.922,10.8613C6.7864,10.991 6.6233,11.0853 6.4445,11.1384C6.2813,11.1836 6.0986,11.1836 5.733,11.1836ZM9.6538,11.1836H11.8785C12.2067,11.1836 12.3718,11.1836 12.4701,11.1188C12.5762,11.05 12.643,10.934 12.6489,10.8083C12.6546,10.6961 12.5758,10.5601 12.4213,10.2935C12.416,10.2845 12.4107,10.2752 12.4052,10.2658L11.2909,8.3595L11.2782,8.3381C11.1216,8.0733 11.0426,7.9395 10.941,7.8878C10.8291,7.8309 10.6973,7.8309 10.5853,7.8878C10.4831,7.9409 10.4006,8.0805 10.2375,8.3615L9.1271,10.2678L9.1233,10.2744C8.9607,10.555 8.8795,10.6952 8.8854,10.8102C8.8932,10.936 8.9581,11.0519 9.0642,11.1207C9.1605,11.1836 9.3256,11.1836 9.6538,11.1836Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#E74142" android:fillType="evenOdd" android:pathData="M16,8C16,12.4183 12.4183,16 8,16C3.5817,16 0,12.4183 0,8C0,3.5817 3.5817,0 8,0C12.4183,0 16,3.5817 16,8ZM5.733,11.1836H4.1804C3.8542,11.1836 3.6931,11.1836 3.5948,11.1207C3.4887,11.0519 3.4238,10.9379 3.416,10.8121C3.4101,10.6962 3.4906,10.5547 3.6518,10.2717L7.4853,3.5146C7.6484,3.2277 7.7309,3.0842 7.8351,3.0312C7.9471,2.9742 8.0808,2.9742 8.1928,3.0312C8.297,3.0842 8.3795,3.2277 8.5426,3.5146L9.3307,4.8903L9.3347,4.8973C9.5109,5.2052 9.6002,5.3613 9.6392,5.5251C9.6825,5.7039 9.6825,5.8926 9.6392,6.0715C9.5999,6.2365 9.5115,6.3938 9.3327,6.7063L7.319,10.2658L7.3138,10.2749C7.1365,10.5853 7.0466,10.7426 6.922,10.8613C6.7864,10.991 6.6233,11.0853 6.4445,11.1384C6.2813,11.1836 6.0986,11.1836 5.733,11.1836ZM9.6538,11.1836H11.8785C12.2067,11.1836 12.3718,11.1836 12.4701,11.1188C12.5762,11.05 12.643,10.934 12.6489,10.8083C12.6546,10.6961 12.5758,10.5601 12.4213,10.2935C12.416,10.2845 12.4107,10.2752 12.4052,10.2658L11.2909,8.3595L11.2782,8.3381C11.1216,8.0733 11.0426,7.9395 10.941,7.8878C10.8291,7.8309 10.6973,7.8309 10.5853,7.8878C10.4831,7.9409 10.4006,8.0805 10.2375,8.3615L9.1271,10.2678L9.1233,10.2744C8.9607,10.555 8.8795,10.6952 8.8854,10.8102C8.8932,10.936 8.9581,11.0519 9.0642,11.1207C9.1605,11.1836 9.3256,11.1836 9.6538,11.1836Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,0L8,0A8,8 0,0 1,16 8L16,8A8,8 0,0 1,8 16L8,16A8,8 0,0 1,0 8L0,8A8,8 0,0 1,8 0z"/>
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M6.0578,7.202L8.0002,5.2604L9.9434,7.2036L11.073,6.0733L8.0002,2.9998L4.9275,6.0725L6.0578,7.202ZM2.999,8.0005L4.129,6.8706L5.259,8.0005L4.129,9.1304L2.999,8.0005ZM8.0002,10.7408L6.0578,8.7985L4.9258,9.9272L4.9274,9.9288L8.0002,13.0007L11.073,9.9272L11.0738,9.9264L9.9434,8.7977L8.0002,10.7408ZM10.74,8.0009L11.87,6.871L12.9999,8.0009L11.87,9.1308L10.74,8.0009ZM8.0004,6.8526L9.1466,7.9995H9.1473L9.1466,8.0003L8.0004,9.1472L6.8542,8.0019L6.8526,7.9995L6.8542,7.9979L7.0549,7.7973L7.1528,7.7001L8.0004,6.8526Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F3BA2F" android:pathData="M8,0L8,0A8,8 0,0 1,16 8L16,8A8,8 0,0 1,8 16L8,16A8,8 0,0 1,0 8L0,8A8,8 0,0 1,8 0z"/>
<path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M6.0578,7.202L8.0002,5.2604L9.9434,7.2036L11.073,6.0733L8.0002,2.9998L4.9275,6.0725L6.0578,7.202ZM2.999,8.0005L4.129,6.8706L5.259,8.0005L4.129,9.1304L2.999,8.0005ZM8.0002,10.7408L6.0578,8.7985L4.9258,9.9272L4.9274,9.9288L8.0002,13.0007L11.073,9.9272L11.0738,9.9264L9.9434,8.7977L8.0002,10.7408ZM10.74,8.0009L11.87,6.871L12.9999,8.0009L11.87,9.1308L10.74,8.0009ZM8.0004,6.8526L9.1466,7.9995H9.1473L9.1466,8.0003L8.0004,9.1472L6.8542,8.0019L6.8526,7.9995L6.8542,7.9979L7.0549,7.7973L7.1528,7.7001L8.0004,6.8526Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M10.6797,5.3445C10.272,4.4259 9.3351,4.2295 8.1882,4.4198L7.8196,3L6.9507,3.223L7.3131,4.6387C7.0845,4.696 6.8498,4.7451 6.6171,4.8106L6.2547,3.403L5.3858,3.626L5.7543,5.0458C5.567,5.099 4,5.498 4,5.498L4.2389,6.4227C4.2389,6.4227 4.8772,6.2447 4.871,6.259C5.2252,6.1669 5.3919,6.3429 5.4702,6.5086L6.4833,10.3998C6.4956,10.5123 6.475,10.7046 6.232,10.77C6.2465,10.7782 5.5999,10.9317 5.5999,10.9317L5.6946,12.0098C5.6946,12.0098 7.2472,11.615 7.451,11.5638L7.8237,13L8.6927,12.777L8.32,11.3306C8.5588,11.2754 8.7915,11.2181 9.018,11.1588L9.3886,12.597L10.2576,12.374L9.8849,10.9399C11.2233,10.6166 12.1684,9.7778 11.9748,8.4951C11.8513,7.7218 11.0009,7.0876 10.2946,7.016C10.7291,6.6334 10.9494,6.0749 10.6797,5.3445ZM10.2617,8.7304C10.4346,10.0008 8.6577,10.1563 8.0708,10.3097L7.5602,8.4092C8.1491,8.2557 9.9693,7.6113 10.2617,8.7304ZM9.191,6.1424C9.3742,7.2717 7.8546,7.4026 7.3646,7.5274L6.8992,5.8028C7.3913,5.6821 8.8183,5.097 9.191,6.1424Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#0AC18E" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<path android:fillColor="#ffffff" android:pathData="M10.6797,5.3445C10.272,4.4259 9.3351,4.2295 8.1882,4.4198L7.8196,3L6.9507,3.223L7.3131,4.6387C7.0845,4.696 6.8498,4.7451 6.6171,4.8106L6.2547,3.403L5.3858,3.626L5.7543,5.0458C5.567,5.099 4,5.498 4,5.498L4.2389,6.4227C4.2389,6.4227 4.8772,6.2447 4.871,6.259C5.2252,6.1669 5.3919,6.3429 5.4702,6.5086L6.4833,10.3998C6.4956,10.5123 6.475,10.7046 6.232,10.77C6.2465,10.7782 5.5999,10.9317 5.5999,10.9317L5.6946,12.0098C5.6946,12.0098 7.2472,11.615 7.451,11.5638L7.8237,13L8.6927,12.777L8.32,11.3306C8.5588,11.2754 8.7915,11.2181 9.018,11.1588L9.3886,12.597L10.2576,12.374L9.8849,10.9399C11.2233,10.6166 12.1684,9.7778 11.9748,8.4951C11.8513,7.7218 11.0009,7.0876 10.2946,7.016C10.7291,6.6334 10.9494,6.0749 10.6797,5.3445ZM10.2617,8.7304C10.4346,10.0008 8.6577,10.1563 8.0708,10.3097L7.5602,8.4092C8.1491,8.2557 9.9693,7.6113 10.2617,8.7304ZM9.191,6.1424C9.3742,7.2717 7.8546,7.4026 7.3646,7.5274L6.8992,5.8028C7.3913,5.6821 8.8183,5.097 9.191,6.1424Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M15.7605,9.9353C14.6919,14.221 10.3507,16.8292 6.064,15.7605C1.7791,14.692 -0.8295,10.351 0.2396,6.0656C1.3077,1.7794 5.649,-0.8291 9.9344,0.2394C14.2209,1.3079 16.8292,5.6493 15.7605,9.9353Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M11.5275,6.8603C11.6868,5.7958 10.8762,5.2236 9.7678,4.8418L10.1273,3.3998L9.2495,3.1811L8.8995,4.5851C8.6687,4.5276 8.4317,4.4733 8.1961,4.4196L8.5487,3.0064L7.6713,2.7876L7.3116,4.2291C7.1205,4.1856 6.933,4.1426 6.751,4.0973L6.752,4.0928L5.5414,3.7906L5.3078,4.7281C5.3078,4.7281 5.9592,4.8773 5.9454,4.8866C6.301,4.9753 6.3652,5.2106 6.3545,5.3971L5.9449,7.0398C5.9694,7.0461 6.0012,7.0551 6.0362,7.0691C6.0069,7.0618 5.9757,7.0538 5.9434,7.0461L5.3693,9.3473C5.3258,9.4553 5.2156,9.6173 4.9671,9.5558C4.9758,9.5685 4.329,9.3965 4.329,9.3965L3.8932,10.4013L5.0356,10.686C5.2481,10.7393 5.4563,10.795 5.6614,10.8475L5.2981,12.306L6.1749,12.5248L6.5347,11.0818C6.7742,11.1468 7.0068,11.2068 7.2343,11.2633L6.8758,12.6995L7.7536,12.9182L8.1169,11.4625C9.6138,11.7458 10.7394,11.6315 11.2132,10.2778C11.595,9.1878 11.1942,8.559 10.4066,8.149C10.9802,8.0168 11.4122,7.6396 11.5275,6.8603ZM9.5218,9.6725C9.2505,10.7625 7.4151,10.1733 6.82,10.0255L7.3021,8.0933C7.8971,8.2418 9.8053,8.5358 9.5218,9.6725ZM9.7933,6.8446C9.5458,7.8361 8.0181,7.3323 7.5226,7.2088L7.9596,5.4563C8.4552,5.5798 10.0511,5.8103 9.7933,6.8446Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F7931A" android:pathData="M15.7605,9.9353C14.6919,14.221 10.3507,16.8292 6.064,15.7605C1.7791,14.692 -0.8295,10.351 0.2396,6.0656C1.3077,1.7794 5.649,-0.8291 9.9344,0.2394C14.2209,1.3079 16.8292,5.6493 15.7605,9.9353Z"/>
<path android:fillColor="#ffffff" android:pathData="M11.5275,6.8603C11.6868,5.7958 10.8762,5.2236 9.7678,4.8418L10.1273,3.3998L9.2495,3.1811L8.8995,4.5851C8.6687,4.5276 8.4317,4.4733 8.1961,4.4196L8.5487,3.0064L7.6713,2.7876L7.3116,4.2291C7.1205,4.1856 6.933,4.1426 6.751,4.0973L6.752,4.0928L5.5414,3.7906L5.3078,4.7281C5.3078,4.7281 5.9592,4.8773 5.9454,4.8866C6.301,4.9753 6.3652,5.2106 6.3545,5.3971L5.9449,7.0398C5.9694,7.0461 6.0012,7.0551 6.0362,7.0691C6.0069,7.0618 5.9757,7.0538 5.9434,7.0461L5.3693,9.3473C5.3258,9.4553 5.2156,9.6173 4.9671,9.5558C4.9758,9.5685 4.329,9.3965 4.329,9.3965L3.8932,10.4013L5.0356,10.686C5.2481,10.7393 5.4563,10.795 5.6614,10.8475L5.2981,12.306L6.1749,12.5248L6.5347,11.0818C6.7742,11.1468 7.0068,11.2068 7.2343,11.2633L6.8758,12.6995L7.7536,12.9182L8.1169,11.4625C9.6138,11.7458 10.7394,11.6315 11.2132,10.2778C11.595,9.1878 11.1942,8.559 10.4066,8.149C10.9802,8.0168 11.4122,7.6396 11.5275,6.8603ZM9.5218,9.6725C9.2505,10.7625 7.4151,10.1733 6.82,10.0255L7.3021,8.0933C7.8971,8.2418 9.8053,8.5358 9.5218,9.6725ZM9.7933,6.8446C9.5458,7.8361 8.0181,7.3323 7.5226,7.2088L7.9596,5.4563C8.4552,5.5798 10.0511,5.8103 9.7933,6.8446Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:fillType="evenOdd" android:pathData="M8,0C12.4186,0 16,3.5814 16,8C16,12.4186 12.4186,16 8,16C3.5814,16 0,12.4186 0,8C0,3.5814 3.5814,0 8,0Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.3968,8.0001L4.4025,10.1154L6.2,11.1731V12.4116L3.3506,10.7404V7.3815L4.3968,8.0001ZM4.3968,5.8847V7.1174L3.35,6.4981V5.2655L4.3968,4.6462L5.4487,5.2655L4.3968,5.8847ZM6.9506,5.2655L7.9974,4.6462L9.0493,5.2655L7.9974,5.8847L6.9506,5.2655Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M5.1532,9.677V8.4385L6.2,9.0577V10.2904L5.1532,9.677ZM6.9506,11.6167L7.9974,12.2359L9.0493,11.6167V12.8494L7.9974,13.4686L6.9506,12.8494V11.6167ZM10.5505,5.2655L11.5973,4.6462L12.6492,5.2655V6.4981L11.5973,7.1174V5.8847L10.5505,5.2655ZM11.5973,10.1154L11.6031,8.0001L12.6499,7.3808V10.7398L9.8006,12.4109V11.1725L11.5973,10.1154Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M10.8468,9.6769L9.8,10.2903V9.0577L10.8468,8.4385V9.6769Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M10.8468,6.3232L10.8526,7.5616L9.05,8.6193V10.7398L8.0032,11.3533L6.9564,10.7398V8.6193L5.1538,7.5616V6.3232L6.2051,5.7039L7.9968,6.7667L9.7993,5.7039L10.8513,6.3232H10.8468ZM5.1532,4.2084L7.9974,2.5315L10.8468,4.2084L9.8,4.8276L7.9974,3.7648L6.2,4.8276L5.1532,4.2084Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:fillType="evenOdd" android:pathData="M8,0C12.4186,0 16,3.5814 16,8C16,12.4186 12.4186,16 8,16C3.5814,16 0,12.4186 0,8C0,3.5814 3.5814,0 8,0Z"/>
<path android:fillColor="#F0B90B" android:pathData="M4.3968,8.0001L4.4025,10.1154L6.2,11.1731V12.4116L3.3506,10.7404V7.3815L4.3968,8.0001ZM4.3968,5.8847V7.1174L3.35,6.4981V5.2655L4.3968,4.6462L5.4487,5.2655L4.3968,5.8847ZM6.9506,5.2655L7.9974,4.6462L9.0493,5.2655L7.9974,5.8847L6.9506,5.2655Z"/>
<path android:fillColor="#F0B90B" android:pathData="M5.1532,9.677V8.4385L6.2,9.0577V10.2904L5.1532,9.677ZM6.9506,11.6167L7.9974,12.2359L9.0493,11.6167V12.8494L7.9974,13.4686L6.9506,12.8494V11.6167ZM10.5505,5.2655L11.5973,4.6462L12.6492,5.2655V6.4981L11.5973,7.1174V5.8847L10.5505,5.2655ZM11.5973,10.1154L11.6031,8.0001L12.6499,7.3808V10.7398L9.8006,12.4109V11.1725L11.5973,10.1154Z"/>
<path android:fillColor="#F0B90B" android:pathData="M10.8468,9.6769L9.8,10.2903V9.0577L10.8468,8.4385V9.6769Z"/>
<path android:fillColor="#F0B90B" android:pathData="M10.8468,6.3232L10.8526,7.5616L9.05,8.6193V10.7398L8.0032,11.3533L6.9564,10.7398V8.6193L5.1538,7.5616V6.3232L6.2051,5.7039L7.9968,6.7667L9.7993,5.7039L10.8513,6.3232H10.8468ZM5.1532,4.2084L7.9974,2.5315L10.8468,4.2084L9.8,4.8276L7.9974,3.7648L6.2,4.8276L5.1532,4.2084Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.8738,3.2662C8.098,3.1493 8.3717,3.4204 8.2526,3.6444C8.1834,3.827 7.9211,3.8755 7.7871,3.7364C7.6452,3.6028 7.6915,3.3388 7.8738,3.2662ZM5.4626,3.5261C5.5988,3.47 5.7712,3.5948 5.7573,3.7432C5.7718,3.9043 5.5747,4.0229 5.4359,3.9464C5.252,3.8751 5.273,3.5766 5.4626,3.5261ZM10.4011,3.9509C10.1883,3.9252 10.1546,3.5906 10.3595,3.5232C10.5184,3.4457 10.6642,3.591 10.6875,3.7417C10.6541,3.8703 10.5465,3.9881 10.4011,3.9509ZM6.0113,4.5523C6.2389,4.4083 6.5638,4.6228 6.5288,4.8872C6.517,5.1543 6.1623,5.309 5.9622,5.1284C5.7687,4.9867 5.798,4.6614 6.0113,4.5523ZM9.4869,4.7051C9.5884,4.4434 10,4.4354 10.1156,4.6903C10.2299,4.8886 10.0894,5.1342 9.8857,5.2016C9.616,5.2519 9.3599,4.9604 9.4869,4.7051ZM7.5835,5.1867C7.5813,4.9617 7.7862,4.7941 8.0001,4.7787C8.1419,4.8072 8.2965,4.8676 8.3573,5.0107C8.4707,5.2082 8.37,5.48 8.1651,5.5706C8.0714,5.6231 7.9607,5.6034 7.8586,5.5973C7.7027,5.5208 7.5713,5.3706 7.5835,5.1867ZM3.9976,5.5146C4.2056,5.3867 4.4862,5.62 4.4075,5.846C4.3641,6.0414 4.1014,6.1209 3.9512,5.9962C3.7909,5.8793 3.8163,5.5982 3.9976,5.5146ZM11.6978,5.5142C11.8742,5.3772 12.1654,5.5343 12.1444,5.7571C12.1562,5.9603 11.9045,6.1135 11.7293,6.0089C11.5364,5.9222 11.5167,5.6253 11.6978,5.5142ZM8.5919,5.9187C8.8831,5.814 9.2289,5.9392 9.3949,6.1988C9.6138,6.5162 9.4974,6.9995 9.1576,7.1816C8.8056,7.397 8.2916,7.2144 8.1615,6.8213C8.014,6.4694 8.229,6.029 8.5919,5.9187ZM6.8834,5.9533C7.2062,5.8009 7.6405,5.9287 7.7964,6.2589C7.9798,6.5802 7.838,7.0276 7.51,7.1935C7.1816,7.38 6.7183,7.2386 6.5594,6.8936C6.3812,6.56 6.5395,6.1069 6.8834,5.9533ZM5.1728,6.5381C5.1973,6.3302 5.3934,6.1949 5.5913,6.1809C5.8046,6.2066 5.9749,6.3722 5.9981,6.588C5.9841,6.8016 5.819,7.0104 5.5926,7.0091C5.3436,7.0306 5.1252,6.7828 5.1728,6.5381ZM10.2255,6.2261C10.4812,6.0773 10.8363,6.2939 10.8249,6.5877C10.8425,6.8989 10.4495,7.1183 10.1949,6.9357C9.9304,6.7837 9.951,6.3555 10.2255,6.2261ZM6.1882,7.3231C6.4872,7.2408 6.8292,7.3818 6.9759,7.6566C7.1365,7.9333 7.0708,8.3177 6.8213,8.5206C6.5013,8.8183 5.911,8.6988 5.7407,8.2926C5.545,7.9201 5.7827,7.4211 6.1882,7.3231ZM9.4286,7.3215C9.7194,7.2384 10.0609,7.3504 10.2172,7.6153C10.4238,7.9182 10.3252,8.37 10.0176,8.5653C9.6953,8.792 9.1961,8.673 9.0188,8.3184C8.8038,7.9517 9.0202,7.4334 9.4286,7.3215ZM4.2647,7.6671C4.523,7.5694 4.8094,7.8545 4.7047,8.1106C4.6408,8.3439 4.312,8.4302 4.1421,8.2581C3.9433,8.0966 4.0169,7.7372 4.2647,7.6671ZM11.2667,8.0388C11.2593,7.8247 11.4159,7.6352 11.6354,7.6234C11.8123,7.6514 11.9843,7.7941 11.9703,7.9867C11.9865,8.2411 11.68,8.414 11.4633,8.2944C11.3617,8.2428 11.3096,8.1386 11.2667,8.0388ZM2.9945,7.7928C3.1499,7.7249 3.3422,7.8596 3.3067,8.0322C3.2983,8.2276 3.0098,8.3028 2.9038,8.1395C2.8102,8.0261 2.8662,7.8512 2.9945,7.7928ZM12.7939,7.7889C12.9033,7.7109 13.0789,7.767 13.1223,7.8948C13.1958,8.0344 13.061,8.2257 12.9038,8.2021C12.6832,8.2222 12.6118,7.886 12.7939,7.7889ZM7.0323,8.727C7.4413,8.6206 7.8809,8.9616 7.8835,9.3827C7.9102,9.8083 7.4772,10.1856 7.0585,10.0873C6.735,10.0352 6.4811,9.7235 6.4915,9.3965C6.4907,9.0838 6.7267,8.7931 7.0323,8.727ZM8.6519,8.7261C9.067,8.6101 9.5197,8.9572 9.5131,9.388C9.5351,9.8057 9.1156,10.1703 8.7057,10.0875C8.32,10.0376 8.0354,9.6138 8.1431,9.2395C8.1974,8.9901 8.4058,8.7872 8.6519,8.7261ZM5.5292,8.9853C5.8165,8.9363 6.0857,9.2567 5.9692,9.5295C5.8843,9.8201 5.4613,9.8989 5.2743,9.6615C5.055,9.4335 5.2187,9.0168 5.5292,8.9853ZM10.2789,8.9981C10.535,8.8908 10.8494,9.1124 10.8297,9.3904C10.84,9.6964 10.4519,9.9074 10.2014,9.7288C9.9177,9.5684 9.9694,9.0965 10.2789,8.9981ZM11.6065,10.3297C11.5062,10.1427 11.6884,9.895 11.8959,9.9296C11.9993,9.9278 12.0715,10.0092 12.1377,10.0775C12.1512,10.1883 12.174,10.3205 12.0821,10.4059C11.9572,10.5639 11.6743,10.5188 11.6065,10.3297ZM3.9968,9.9816C4.1907,9.8525 4.4699,10.0451 4.4263,10.2719C4.4031,10.4725 4.1504,10.5871 3.984,10.4733C3.8054,10.37 3.812,10.0758 3.9968,9.9816ZM7.8292,10.4015C8.0832,10.2894 8.4067,10.5105 8.3826,10.7902C8.4015,11.0993 8.0013,11.3134 7.753,11.1278C7.4772,10.9684 7.5232,10.4995 7.8292,10.4015ZM6.0542,10.8156C6.2705,10.7018 6.5607,10.8983 6.541,11.1404C6.5476,11.3392 6.3536,11.5086 6.1575,11.4792C5.995,11.4757 5.8891,11.3301 5.837,11.1916C5.8396,11.0406 5.9018,10.8725 6.0542,10.8156ZM9.6497,10.8191C9.8722,10.6816 10.1883,10.8791 10.1673,11.1379C10.173,11.4085 9.8157,11.5818 9.6086,11.4041C9.4081,11.2648 9.4326,10.9299 9.6497,10.8191ZM10.275,12.3649C10.1761,12.2152 10.2925,12.0348 10.4532,12.0007C10.5824,12.0269 10.719,12.1241 10.6918,12.2739C10.673,12.4765 10.3766,12.5387 10.275,12.3649ZM5.3116,12.2349C5.351,12.1162 5.4464,11.9983 5.5861,12.0292C5.7897,12.0436 5.8501,12.3505 5.6719,12.4451C5.5117,12.5531 5.3258,12.4051 5.3116,12.2349ZM7.7062,12.371C7.7451,12.1955 7.9723,12.1101 8.1221,12.2033C8.2211,12.2458 8.25,12.3565 8.2754,12.4507C8.2618,12.4997 8.2496,12.5487 8.2377,12.5978C8.1764,12.6704 8.0976,12.7431 7.9956,12.7405C7.806,12.7627 7.6349,12.5483 7.7062,12.371Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#0033AD" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#ffffff" android:pathData="M7.8738,3.2662C8.098,3.1493 8.3717,3.4204 8.2526,3.6444C8.1834,3.827 7.9211,3.8755 7.7871,3.7364C7.6452,3.6028 7.6915,3.3388 7.8738,3.2662ZM5.4626,3.5261C5.5988,3.47 5.7712,3.5948 5.7573,3.7432C5.7718,3.9043 5.5747,4.0229 5.4359,3.9464C5.252,3.8751 5.273,3.5766 5.4626,3.5261ZM10.4011,3.9509C10.1883,3.9252 10.1546,3.5906 10.3595,3.5232C10.5184,3.4457 10.6642,3.591 10.6875,3.7417C10.6541,3.8703 10.5465,3.9881 10.4011,3.9509ZM6.0113,4.5523C6.2389,4.4083 6.5638,4.6228 6.5288,4.8872C6.517,5.1543 6.1623,5.309 5.9622,5.1284C5.7687,4.9867 5.798,4.6614 6.0113,4.5523ZM9.4869,4.7051C9.5884,4.4434 10,4.4354 10.1156,4.6903C10.2299,4.8886 10.0894,5.1342 9.8857,5.2016C9.616,5.2519 9.3599,4.9604 9.4869,4.7051ZM7.5835,5.1867C7.5813,4.9617 7.7862,4.7941 8.0001,4.7787C8.1419,4.8072 8.2965,4.8676 8.3573,5.0107C8.4707,5.2082 8.37,5.48 8.1651,5.5706C8.0714,5.6231 7.9607,5.6034 7.8586,5.5973C7.7027,5.5208 7.5713,5.3706 7.5835,5.1867ZM3.9976,5.5146C4.2056,5.3867 4.4862,5.62 4.4075,5.846C4.3641,6.0414 4.1014,6.1209 3.9512,5.9962C3.7909,5.8793 3.8163,5.5982 3.9976,5.5146ZM11.6978,5.5142C11.8742,5.3772 12.1654,5.5343 12.1444,5.7571C12.1562,5.9603 11.9045,6.1135 11.7293,6.0089C11.5364,5.9222 11.5167,5.6253 11.6978,5.5142ZM8.5919,5.9187C8.8831,5.814 9.2289,5.9392 9.3949,6.1988C9.6138,6.5162 9.4974,6.9995 9.1576,7.1816C8.8056,7.397 8.2916,7.2144 8.1615,6.8213C8.014,6.4694 8.229,6.029 8.5919,5.9187ZM6.8834,5.9533C7.2062,5.8009 7.6405,5.9287 7.7964,6.2589C7.9798,6.5802 7.838,7.0276 7.51,7.1935C7.1816,7.38 6.7183,7.2386 6.5594,6.8936C6.3812,6.56 6.5395,6.1069 6.8834,5.9533ZM5.1728,6.5381C5.1973,6.3302 5.3934,6.1949 5.5913,6.1809C5.8046,6.2066 5.9749,6.3722 5.9981,6.588C5.9841,6.8016 5.819,7.0104 5.5926,7.0091C5.3436,7.0306 5.1252,6.7828 5.1728,6.5381ZM10.2255,6.2261C10.4812,6.0773 10.8363,6.2939 10.8249,6.5877C10.8425,6.8989 10.4495,7.1183 10.1949,6.9357C9.9304,6.7837 9.951,6.3555 10.2255,6.2261ZM6.1882,7.3231C6.4872,7.2408 6.8292,7.3818 6.9759,7.6566C7.1365,7.9333 7.0708,8.3177 6.8213,8.5206C6.5013,8.8183 5.911,8.6988 5.7407,8.2926C5.545,7.9201 5.7827,7.4211 6.1882,7.3231ZM9.4286,7.3215C9.7194,7.2384 10.0609,7.3504 10.2172,7.6153C10.4238,7.9182 10.3252,8.37 10.0176,8.5653C9.6953,8.792 9.1961,8.673 9.0188,8.3184C8.8038,7.9517 9.0202,7.4334 9.4286,7.3215ZM4.2647,7.6671C4.523,7.5694 4.8094,7.8545 4.7047,8.1106C4.6408,8.3439 4.312,8.4302 4.1421,8.2581C3.9433,8.0966 4.0169,7.7372 4.2647,7.6671ZM11.2667,8.0388C11.2593,7.8247 11.4159,7.6352 11.6354,7.6234C11.8123,7.6514 11.9843,7.7941 11.9703,7.9867C11.9865,8.2411 11.68,8.414 11.4633,8.2944C11.3617,8.2428 11.3096,8.1386 11.2667,8.0388ZM2.9945,7.7928C3.1499,7.7249 3.3422,7.8596 3.3067,8.0322C3.2983,8.2276 3.0098,8.3028 2.9038,8.1395C2.8102,8.0261 2.8662,7.8512 2.9945,7.7928ZM12.7939,7.7889C12.9033,7.7109 13.0789,7.767 13.1223,7.8948C13.1958,8.0344 13.061,8.2257 12.9038,8.2021C12.6832,8.2222 12.6118,7.886 12.7939,7.7889ZM7.0323,8.727C7.4413,8.6206 7.8809,8.9616 7.8835,9.3827C7.9102,9.8083 7.4772,10.1856 7.0585,10.0873C6.735,10.0352 6.4811,9.7235 6.4915,9.3965C6.4907,9.0838 6.7267,8.7931 7.0323,8.727ZM8.6519,8.7261C9.067,8.6101 9.5197,8.9572 9.5131,9.388C9.5351,9.8057 9.1156,10.1703 8.7057,10.0875C8.32,10.0376 8.0354,9.6138 8.1431,9.2395C8.1974,8.9901 8.4058,8.7872 8.6519,8.7261ZM5.5292,8.9853C5.8165,8.9363 6.0857,9.2567 5.9692,9.5295C5.8843,9.8201 5.4613,9.8989 5.2743,9.6615C5.055,9.4335 5.2187,9.0168 5.5292,8.9853ZM10.2789,8.9981C10.535,8.8908 10.8494,9.1124 10.8297,9.3904C10.84,9.6964 10.4519,9.9074 10.2014,9.7288C9.9177,9.5684 9.9694,9.0965 10.2789,8.9981ZM11.6065,10.3297C11.5062,10.1427 11.6884,9.895 11.8959,9.9296C11.9993,9.9278 12.0715,10.0092 12.1377,10.0775C12.1512,10.1883 12.174,10.3205 12.0821,10.4059C11.9572,10.5639 11.6743,10.5188 11.6065,10.3297ZM3.9968,9.9816C4.1907,9.8525 4.4699,10.0451 4.4263,10.2719C4.4031,10.4725 4.1504,10.5871 3.984,10.4733C3.8054,10.37 3.812,10.0758 3.9968,9.9816ZM7.8292,10.4015C8.0832,10.2894 8.4067,10.5105 8.3826,10.7902C8.4015,11.0993 8.0013,11.3134 7.753,11.1278C7.4772,10.9684 7.5232,10.4995 7.8292,10.4015ZM6.0542,10.8156C6.2705,10.7018 6.5607,10.8983 6.541,11.1404C6.5476,11.3392 6.3536,11.5086 6.1575,11.4792C5.995,11.4757 5.8891,11.3301 5.837,11.1916C5.8396,11.0406 5.9018,10.8725 6.0542,10.8156ZM9.6497,10.8191C9.8722,10.6816 10.1883,10.8791 10.1673,11.1379C10.173,11.4085 9.8157,11.5818 9.6086,11.4041C9.4081,11.2648 9.4326,10.9299 9.6497,10.8191ZM10.275,12.3649C10.1761,12.2152 10.2925,12.0348 10.4532,12.0007C10.5824,12.0269 10.719,12.1241 10.6918,12.2739C10.673,12.4765 10.3766,12.5387 10.275,12.3649ZM5.3116,12.2349C5.351,12.1162 5.4464,11.9983 5.5861,12.0292C5.7897,12.0436 5.8501,12.3505 5.6719,12.4451C5.5117,12.5531 5.3258,12.4051 5.3116,12.2349ZM7.7062,12.371C7.7451,12.1955 7.9723,12.1101 8.1221,12.2033C8.2211,12.2458 8.25,12.3565 8.2754,12.4507C8.2618,12.4997 8.2496,12.5487 8.2377,12.5978C8.1764,12.6704 8.0976,12.7431 7.9956,12.7405C7.806,12.7627 7.6349,12.5483 7.7062,12.371Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M5.3545,12.185H8.1164H8.1165C8.1165,12.185 11.9999,12.5157 11.9999,8.0674C11.9999,3.7917 8.4536,3.8068 7.7682,3.8098C7.7521,3.8098 7.7376,3.8099 7.7248,3.8099H5.3544V7.5406H4.3809V8.4545H5.3545V12.185ZM6.9101,5.3579H7.9998C8.4074,5.3579 10.4578,5.5245 10.4611,8.0975C10.4643,10.6399 8.397,10.6373 8.0645,10.6368C8.0604,10.6368 8.0567,10.6368 8.0532,10.6368H6.9101V8.4544H8.6245V7.5405H6.9101V5.3579Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#C2A633" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M5.3545,12.185H8.1164H8.1165C8.1165,12.185 11.9999,12.5157 11.9999,8.0674C11.9999,3.7917 8.4536,3.8068 7.7682,3.8098C7.7521,3.8098 7.7376,3.8099 7.7248,3.8099H5.3544V7.5406H4.3809V8.4545H5.3545V12.185ZM6.9101,5.3579H7.9998C8.4074,5.3579 10.4578,5.5245 10.4611,8.0975C10.4643,10.6399 8.397,10.6373 8.0645,10.6368C8.0604,10.6368 8.0567,10.6368 8.0532,10.6368H6.9101V8.4544H8.6245V7.5405H6.9101V5.3579Z"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<group>
<clip-path android:pathData="M8.0002,2.8001L8.0002,2.8001A3.2001,3.2001 0,0 1,11.2003 6.0001L11.2003,10.0003A3.2001,3.2001 0,0 1,8.0002 13.2003L8.0002,13.2003A3.2001,3.2001 0,0 1,4.8002 10.0003L4.8002,6.0001A3.2001,3.2001 0,0 1,8.0002 2.8001z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9994,2.8001L7.9296,3.037V9.9123L7.9994,9.9819L11.1986,8.0955L7.9994,2.8001Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9993,2.8001L4.8002,8.0955L7.9993,9.9819V6.6449V2.8001Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9992,10.5862L7.9598,10.6341V13.0832L7.9992,13.198L11.2003,8.7007L7.9992,10.5862Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9993,13.1982V10.5864L4.8002,8.7009L7.9993,13.1982Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9991,9.9821L11.1982,8.0956L7.9991,6.645V9.9821Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.8002,8.0956L7.9992,9.9821V6.645L4.8002,8.0956Z"/>
</group>
</vector>

View file

@ -0,0 +1,20 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<group>
<clip-path android:pathData="M8,2.8001L8,2.8001A3.2,3.2 0,0 1,11.2001 6.0001L11.2001,10A3.2,3.2 0,0 1,8 13.2001L8,13.2001A3.2,3.2 0,0 1,4.8 10L4.8,6.0001A3.2,3.2 0,0 1,8 2.8001z"/>
<path android:fillAlpha="0.8" android:fillColor="#343434"
android:pathData="M7.9992,2.8001L7.9293,3.037V9.9122L7.9992,9.9817L11.1982,8.0953L7.9992,2.8001Z" android:strokeAlpha="0.8"/>
<path android:fillAlpha="0.8" android:fillColor="#8C8C8C"
android:pathData="M7.9991,2.8001L4.8,8.0953L7.9991,9.9818V6.6448V2.8001Z" android:strokeAlpha="0.8"/>
<path android:fillAlpha="0.8" android:fillColor="#3C3C3B"
android:pathData="M7.999,10.5862L7.9596,10.634V13.0831L7.999,13.1978L11.1999,8.7007L7.999,10.5862Z" android:strokeAlpha="0.8"/>
<path android:fillAlpha="0.8" android:fillColor="#8C8C8C"
android:pathData="M7.9991,13.1979V10.5862L4.8,8.7007L7.9991,13.1979Z" android:strokeAlpha="0.8"/>
<path android:fillAlpha="0.8" android:fillColor="#141414"
android:pathData="M7.999,9.9817L11.198,8.0953L7.999,6.6448V9.9817Z" android:strokeAlpha="0.8"/>
<path android:fillAlpha="0.8" android:fillColor="#393939"
android:pathData="M4.8,8.0953L7.9991,9.9818V6.6448L4.8,8.0953Z" android:strokeAlpha="0.8"/>
</group>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M11,22C17.0751,22 22,17.0751 22,11C22,4.9249 17.0751,0 11,0C4.9249,0 0,4.9249 0,11C0,17.0751 4.9249,22 11,22Z"/>
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M11.825,8.8687L14.3,7.425V10.3125L11.825,8.8687ZM14.3,15.0562L11,16.9813L7.7,15.0562V11.6875L11,13.6125L14.3,11.6875V15.0562ZM7.7,7.425L10.175,8.8687L7.7,10.3125V7.425ZM11.4125,9.5562L13.8875,11L11.4125,12.4438V9.5562ZM10.5875,12.4438L8.1125,11L10.5875,9.5562V12.4438ZM13.8875,6.7375L11,8.3875L8.1125,6.7375L11,5.0188L13.8875,6.7375ZM6.875,6.4625V15.4688L11,17.8062L15.125,15.4688V6.4625L11,4.125L6.875,6.4625Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#13B5EC" android:pathData="M11,22C17.0751,22 22,17.0751 22,11C22,4.9249 17.0751,0 11,0C4.9249,0 0,4.9249 0,11C0,17.0751 4.9249,22 11,22Z"/>
<path android:fillColor="#F8F9F9" android:fillType="evenOdd" android:pathData="M11.825,8.8687L14.3,7.425V10.3125L11.825,8.8687ZM14.3,15.0562L11,16.9813L7.7,15.0562V11.6875L11,13.6125L14.3,11.6875V15.0562ZM7.7,7.425L10.175,8.8687L7.7,10.3125V7.425ZM11.4125,9.5562L13.8875,11L11.4125,12.4438V9.5562ZM10.5875,12.4438L8.1125,11L10.5875,9.5562V12.4438ZM13.8875,6.7375L11,8.3875L8.1125,6.7375L11,5.0188L13.8875,6.7375ZM6.875,6.4625V15.4688L11,17.8062L15.125,15.4688V6.4625L11,4.125L6.875,6.4625Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#A1A1A4" android:pathData="M12.4155,5.3206C12.265,5.2041 12.0856,5.0449 11.7587,5.0051C11.452,4.9653 11.1395,5.1671 10.9282,5.3007C10.717,5.4343 10.3177,5.8266 10.1528,5.946C9.9879,6.0654 9.5654,6.1762 8.8854,6.577C8.2054,6.9778 5.5376,8.6606 5.5376,8.6606L6.2321,8.6691L3.136,10.2354H3.4456L3,10.5679C3,10.5679 3.3935,10.6703 3.7234,10.4656V10.5594C3.7234,10.5594 7.4097,9.1325 8.1215,9.502L7.6875,9.6271C7.7251,9.6271 8.4254,9.6725 8.4254,9.6725C8.4423,9.815 8.4911,9.952 8.5681,10.0738C8.6451,10.1957 8.7485,10.2994 8.8709,10.3775C9.2934,10.6504 9.3021,10.801 9.3021,10.801C9.3021,10.801 9.0822,10.8891 9.0822,11C9.0822,11 9.4063,10.9034 9.7072,10.9119C9.8982,10.919 10.0876,10.9486 10.2714,11C10.2714,11 10.2483,10.8806 9.956,10.801C9.6638,10.7214 9.3744,10.4088 9.2326,10.2382C9.1458,10.1296 9.0892,10.0008 9.0684,9.8642C9.0476,9.7276 9.0633,9.5881 9.114,9.4593C9.2153,9.2007 9.5683,9.0585 10.2975,8.689C11.1568,8.2513 11.3536,7.9272 11.4751,7.6742C11.5966,7.4212 11.776,6.9181 11.8773,6.6822C12.0046,6.378 12.1609,6.216 12.2911,6.1194C12.4213,6.0227 13,5.8095 13,5.8095C13,5.8095 12.5573,5.4315 12.4155,5.3206Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#ffffff" android:pathData="M12.4155,5.3206C12.265,5.2041 12.0856,5.0449 11.7587,5.0051C11.452,4.9653 11.1395,5.1671 10.9282,5.3007C10.717,5.4343 10.3177,5.8266 10.1528,5.946C9.9879,6.0654 9.5654,6.1762 8.8854,6.577C8.2054,6.9778 5.5376,8.6606 5.5376,8.6606L6.2321,8.6691L3.136,10.2354H3.4456L3,10.5679C3,10.5679 3.3935,10.6703 3.7234,10.4656V10.5594C3.7234,10.5594 7.4097,9.1325 8.1215,9.502L7.6875,9.6271C7.7251,9.6271 8.4254,9.6725 8.4254,9.6725C8.4423,9.815 8.4911,9.952 8.5681,10.0738C8.6451,10.1957 8.7485,10.2994 8.8709,10.3775C9.2934,10.6504 9.3021,10.801 9.3021,10.801C9.3021,10.801 9.0822,10.8891 9.0822,11C9.0822,11 9.4063,10.9034 9.7072,10.9119C9.8982,10.919 10.0876,10.9486 10.2714,11C10.2714,11 10.2483,10.8806 9.956,10.801C9.6638,10.7214 9.3744,10.4088 9.2326,10.2382C9.1458,10.1296 9.0892,10.0008 9.0684,9.8642C9.0476,9.7276 9.0633,9.5881 9.114,9.4593C9.2153,9.2007 9.5683,9.0585 10.2975,8.689C11.1568,8.2513 11.3536,7.9272 11.4751,7.6742C11.5966,7.4212 11.776,6.9181 11.8773,6.6822C12.0046,6.378 12.1609,6.216 12.2911,6.1194C12.4213,6.0227 13,5.8095 13,5.8095C13,5.8095 12.5573,5.4315 12.4155,5.3206Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector android:autoMirrored="true" android:height="38dp"
android:viewportHeight="38" android:viewportWidth="32"
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M1,0L1,14C1,25.0457 9.9543,34 21,34H30"
android:strokeColor="#DEDEDE" android:strokeWidth="1"/>
<path android:fillColor="#DEDEDE"
android:pathData="M26.75,31.8822V36.1185L30.8871,34.0004L26.75,31.8822Z"
android:strokeColor="#DEDEDE" android:strokeWidth="1.5"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector android:autoMirrored="true" android:height="89dp"
android:viewportHeight="89" android:viewportWidth="32"
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M1,0L1,65C1,76.0457 9.9543,85 21,85H30"
android:strokeColor="#DEDEDE" android:strokeWidth="1"/>
<path android:fillColor="#DEDEDE"
android:pathData="M26.75,82.8822V87.1185L30.8871,85.0004L26.75,82.8822Z"
android:strokeColor="#DEDEDE" android:strokeWidth="1.5"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h16v16h-16z"/>
<path android:fillColor="#A1A1A4" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<path android:fillColor="#F1F1F1" android:pathData="M8,0C6.4177,0 4.871,0.4692 3.5554,1.3482C2.2399,2.2273 1.2145,3.4767 0.609,4.9385C0.0035,6.4003 -0.155,8.0089 0.1537,9.5607C0.4624,11.1126 1.2243,12.538 2.3431,13.6569C3.462,14.7757 4.8874,15.5376 6.4393,15.8463C7.9911,16.155 9.5997,15.9965 11.0615,15.391C12.5233,14.7855 13.7727,13.7602 14.6518,12.4446C15.5308,11.129 16,9.5822 16,8C16.0031,6.9525 15.7998,5.9146 15.4017,4.9457C15.0037,3.9767 14.4187,3.0956 13.6801,2.3528C12.9416,1.6099 12.0639,1.0198 11.0973,0.6161C10.1307,0.2124 9.094,0.003 8.0465,0H8ZM8.1356,8.2712L7.3027,11.0799H11.7579C11.7874,11.0789 11.8168,11.0837 11.8445,11.094C11.8721,11.1044 11.8975,11.12 11.9191,11.1402C11.9407,11.1603 11.9581,11.1846 11.9703,11.2114C11.9825,11.2383 11.9893,11.2673 11.9903,11.2969V11.3705L11.6029,12.707C11.5858,12.7702 11.5478,12.8257 11.495,12.8645C11.4422,12.9032 11.3778,12.9228 11.3124,12.9201H4.4939L5.6368,9.0266L4.3584,9.414L4.6489,8.523L5.9274,8.1356L7.5351,2.6731C7.5528,2.6103 7.591,2.5552 7.6437,2.5165C7.6963,2.4779 7.7604,2.458 7.8257,2.4601H9.5496C9.5792,2.459 9.6086,2.4638 9.6362,2.4741C9.6639,2.4845 9.6892,2.5002 9.7108,2.5203C9.7324,2.5405 9.7498,2.5647 9.762,2.5916C9.7743,2.6185 9.7811,2.6475 9.7821,2.677V2.7506L8.4262,7.3608L9.7046,6.9734L9.4334,7.9032L8.1356,8.2712Z"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h16v16h-16z"/>
<path android:fillColor="#ffffff" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<path android:fillColor="#345D9D" android:pathData="M8,0C6.4177,0 4.871,0.4692 3.5554,1.3482C2.2399,2.2273 1.2145,3.4767 0.609,4.9385C0.0035,6.4003 -0.155,8.0089 0.1537,9.5607C0.4624,11.1126 1.2243,12.538 2.3431,13.6569C3.462,14.7757 4.8874,15.5376 6.4393,15.8463C7.9911,16.155 9.5997,15.9965 11.0615,15.391C12.5233,14.7855 13.7727,13.7602 14.6518,12.4446C15.5308,11.129 16,9.5822 16,8C16.0031,6.9525 15.7998,5.9146 15.4017,4.9457C15.0037,3.9767 14.4187,3.0956 13.6801,2.3528C12.9416,1.6099 12.0639,1.0198 11.0973,0.6161C10.1307,0.2124 9.094,0.003 8.0465,0H8ZM8.1356,8.2712L7.3027,11.0799H11.7579C11.7874,11.0789 11.8168,11.0837 11.8445,11.094C11.8721,11.1044 11.8975,11.12 11.9191,11.1402C11.9407,11.1603 11.9581,11.1846 11.9703,11.2114C11.9825,11.2383 11.9893,11.2673 11.9903,11.2969V11.3705L11.6029,12.707C11.5858,12.7702 11.5478,12.8257 11.495,12.8645C11.4422,12.9032 11.3778,12.9228 11.3124,12.9201H4.4939L5.6368,9.0266L4.3584,9.414L4.6489,8.523L5.9274,8.1356L7.5351,2.6731C7.5528,2.6103 7.591,2.5552 7.6437,2.5165C7.6963,2.4779 7.7604,2.458 7.8257,2.4601H9.5496C9.5792,2.459 9.6086,2.4638 9.6362,2.4741C9.6639,2.4845 9.6892,2.5002 9.7108,2.5203C9.7324,2.5405 9.7498,2.5647 9.762,2.5916C9.7743,2.6185 9.7811,2.6475 9.7821,2.677V2.7506L8.4262,7.3608L9.7046,6.9734L9.4334,7.9032L8.1356,8.2712Z"/>
</group>
</vector>

View file

@ -0,0 +1,11 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9994,4.5169C9.2703,4.5169 10.3006,3.9535 10.3006,3.2585C10.3006,2.5634 9.2703,2 7.9994,2C6.7285,2 5.6982,2.5634 5.6982,3.2585C5.6982,3.9535 6.7285,4.5169 7.9994,4.5169Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9994,13.9991C9.2703,13.9991 10.3006,13.4357 10.3006,12.7406C10.3006,12.0456 9.2703,11.4822 7.9994,11.4822C6.7285,11.4822 5.6982,12.0456 5.6982,12.7406C5.6982,13.4357 6.7285,13.9991 7.9994,13.9991Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.7927,6.259C5.4282,5.2241 5.424,4.1032 4.7835,3.7555C4.1429,3.4077 3.1086,3.9648 2.4731,4.9997C1.8376,6.0346 1.8418,7.1554 2.4823,7.5032C3.1228,7.8509 4.1572,7.2939 4.7927,6.259Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M13.5251,10.9994C14.1605,9.9645 14.1567,8.8438 13.5166,8.4963C12.8765,8.1488 11.8425,8.706 11.207,9.7409C10.5715,10.7758 10.5753,11.8965 11.2154,12.244C11.8555,12.5915 12.8896,12.0343 13.5251,10.9994Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.7837,12.2438C5.4243,11.896 5.4284,10.7752 4.793,9.7403C4.1575,8.7054 3.1231,8.1483 2.4826,8.4961C1.842,8.8438 1.8379,9.9647 2.4733,10.9996C3.1088,12.0345 4.1432,12.5915 4.7837,12.2438Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M13.517,7.5035C14.1571,7.156 14.1609,6.0353 13.5255,5.0004C12.89,3.9655 11.856,3.4083 11.2159,3.7558C10.5758,4.1033 10.572,5.224 11.2075,6.2589C11.8429,7.2938 12.8769,7.851 13.517,7.5035Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FEF2F8" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#E6007A" android:pathData="M7.9994,4.5169C9.2703,4.5169 10.3006,3.9535 10.3006,3.2585C10.3006,2.5634 9.2703,2 7.9994,2C6.7285,2 5.6982,2.5634 5.6982,3.2585C5.6982,3.9535 6.7285,4.5169 7.9994,4.5169Z"/>
<path android:fillColor="#E6007A" android:pathData="M7.9994,13.9991C9.2703,13.9991 10.3006,13.4357 10.3006,12.7406C10.3006,12.0456 9.2703,11.4822 7.9994,11.4822C6.7285,11.4822 5.6982,12.0456 5.6982,12.7406C5.6982,13.4357 6.7285,13.9991 7.9994,13.9991Z"/>
<path android:fillColor="#E6007A" android:pathData="M4.7927,6.259C5.4282,5.2241 5.424,4.1032 4.7835,3.7555C4.1429,3.4077 3.1086,3.9648 2.4731,4.9997C1.8376,6.0346 1.8418,7.1554 2.4823,7.5032C3.1228,7.8509 4.1572,7.2939 4.7927,6.259Z"/>
<path android:fillColor="#E6007A" android:pathData="M13.5251,10.9994C14.1605,9.9645 14.1567,8.8438 13.5166,8.4963C12.8765,8.1488 11.8425,8.706 11.207,9.7409C10.5715,10.7758 10.5753,11.8965 11.2154,12.244C11.8555,12.5915 12.8896,12.0343 13.5251,10.9994Z"/>
<path android:fillColor="#E6007A" android:pathData="M4.7837,12.2438C5.4243,11.896 5.4284,10.7752 4.793,9.7403C4.1575,8.7054 3.1231,8.1483 2.4826,8.4961C1.842,8.8438 1.8379,9.9647 2.4733,10.9996C3.1088,12.0345 4.1432,12.5915 4.7837,12.2438Z"/>
<path android:fillColor="#E6007A" android:pathData="M13.517,7.5035C14.1571,7.156 14.1609,6.0353 13.5255,5.0004C12.89,3.9655 11.856,3.4083 11.2159,3.7558C10.5758,4.1033 10.572,5.224 11.2075,6.2589C11.8429,7.2938 12.8769,7.851 13.517,7.5035Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M10.6283,6.3937C10.4378,6.2889 10.1902,6.2889 9.9808,6.3937L8.4952,7.1968L7.4858,7.7205L6.0003,8.5237C5.8098,8.6284 5.5622,8.6284 5.3527,8.5237L4.1719,7.8951C3.9815,7.7904 3.8482,7.5983 3.8482,7.3888V6.1492C3.8482,5.9397 3.9624,5.7477 4.1719,5.6429L5.3337,5.0319C5.5241,4.9271 5.7717,4.9271 5.9812,5.0319L7.143,5.6429C7.3335,5.7477 7.4668,5.9397 7.4668,6.1492V6.9524L8.4762,6.4111V5.608C8.4762,5.3985 8.3619,5.2064 8.1524,5.1017L6.0003,3.9494C5.8098,3.8447 5.5622,3.8447 5.3527,3.9494L3.1625,5.1017C2.953,5.2064 2.8387,5.3985 2.8387,5.608V7.93C2.8387,8.1395 2.953,8.3316 3.1625,8.4363L5.3527,9.5886C5.5432,9.6934 5.7908,9.6934 6.0003,9.5886L7.4858,8.803L8.4952,8.2618L9.9808,7.4761C10.1712,7.3714 10.4188,7.3714 10.6283,7.4761L11.7901,8.0872C11.9805,8.1919 12.1138,8.384 12.1138,8.5935V9.8331C12.1138,10.0426 11.9996,10.2346 11.7901,10.3394L10.6283,10.9679C10.4378,11.0727 10.1902,11.0727 9.9808,10.9679L8.819,10.3568C8.6285,10.2521 8.4952,10.06 8.4952,9.8505V9.0474L7.4858,9.5886V10.3917C7.4858,10.6013 7.6001,10.7933 7.8096,10.8981L9.9998,12.0504C10.1902,12.1551 10.4378,12.1551 10.6473,12.0504L12.8376,10.8981C13.028,10.7933 13.1613,10.6013 13.1613,10.3917V8.0697C13.1613,7.8602 13.0471,7.6682 12.8376,7.5634L10.6283,6.3937Z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#8247E5" android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"/>
<path android:fillColor="#ffffff" android:pathData="M10.6283,6.3937C10.4378,6.2889 10.1902,6.2889 9.9808,6.3937L8.4952,7.1968L7.4858,7.7205L6.0003,8.5237C5.8098,8.6284 5.5622,8.6284 5.3527,8.5237L4.1719,7.8951C3.9815,7.7904 3.8482,7.5983 3.8482,7.3888V6.1492C3.8482,5.9397 3.9624,5.7477 4.1719,5.6429L5.3337,5.0319C5.5241,4.9271 5.7717,4.9271 5.9812,5.0319L7.143,5.6429C7.3335,5.7477 7.4668,5.9397 7.4668,6.1492V6.9524L8.4762,6.4111V5.608C8.4762,5.3985 8.3619,5.2064 8.1524,5.1017L6.0003,3.9494C5.8098,3.8447 5.5622,3.8447 5.3527,3.9494L3.1625,5.1017C2.953,5.2064 2.8387,5.3985 2.8387,5.608V7.93C2.8387,8.1395 2.953,8.3316 3.1625,8.4363L5.3527,9.5886C5.5432,9.6934 5.7908,9.6934 6.0003,9.5886L7.4858,8.803L8.4952,8.2618L9.9808,7.4761C10.1712,7.3714 10.4188,7.3714 10.6283,7.4761L11.7901,8.0872C11.9805,8.1919 12.1138,8.384 12.1138,8.5935V9.8331C12.1138,10.0426 11.9996,10.2346 11.7901,10.3394L10.6283,10.9679C10.4378,11.0727 10.1902,11.0727 9.9808,10.9679L8.819,10.3568C8.6285,10.2521 8.4952,10.06 8.4952,9.8505V9.0474L7.4858,9.5886V10.3917C7.4858,10.6013 7.6001,10.7933 7.8096,10.8981L9.9998,12.0504C10.1902,12.1551 10.4378,12.1551 10.6473,12.0504L12.8376,10.8981C13.028,10.7933 13.1613,10.6013 13.1613,10.3917V8.0697C13.1613,7.8602 13.0471,7.6682 12.8376,7.5634L10.6283,6.3937Z"/>
</vector>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,0L8,0A8,8 0,0 1,16 8L16,8A8,8 0,0 1,8 16L8,16A8,8 0,0 1,0 8L0,8A8,8 0,0 1,8 0z"/>
<path android:fillColor="#A1A1A4" android:pathData="M12.3419,10.4366L10.8935,12.0202C10.862,12.0546 10.8239,12.082 10.7816,12.1008C10.7392,12.1195 10.6936,12.1292 10.6474,12.1292H3.7811C3.7483,12.1292 3.7163,12.1194 3.6889,12.1011C3.6615,12.0828 3.6399,12.0567 3.6268,12.0261C3.6138,11.9954 3.6098,11.9616 3.6154,11.9287C3.6209,11.8957 3.6358,11.8652 3.6581,11.8408L5.1077,10.2572C5.1391,10.2228 5.177,10.1955 5.2192,10.1767C5.2614,10.158 5.307,10.1483 5.353,10.1482H12.219C12.2517,10.1482 12.2838,10.158 12.3112,10.1763C12.3385,10.1946 12.3601,10.2207 12.3732,10.2513C12.3862,10.2819 12.3902,10.3158 12.3847,10.3487C12.3791,10.3816 12.3643,10.4122 12.3419,10.4366ZM10.8935,7.2476C10.862,7.2132 10.8239,7.1858 10.7816,7.167C10.7392,7.1483 10.6936,7.1386 10.6474,7.1387H3.7811C3.7483,7.1387 3.7163,7.1484 3.6889,7.1667C3.6615,7.1851 3.6399,7.2111 3.6268,7.2418C3.6138,7.2724 3.6098,7.3063 3.6154,7.3392C3.6209,7.3721 3.6358,7.4026 3.6581,7.4271L5.1077,9.0107C5.1391,9.045 5.177,9.0723 5.2192,9.0911C5.2614,9.1098 5.307,9.1195 5.353,9.1196H12.219C12.2517,9.1196 12.2838,9.1099 12.3112,9.0915C12.3385,9.0732 12.3601,9.0472 12.3732,9.0165C12.3862,8.9859 12.3902,8.952 12.3847,8.9191C12.3791,8.8862 12.3643,8.8557 12.3419,8.8312L10.8935,7.2476ZM3.7811,6.1101H10.6474C10.6936,6.1101 10.7392,6.1005 10.7816,6.0817C10.8239,6.063 10.862,6.0356 10.8935,6.0012L12.3419,4.4175C12.3643,4.3931 12.3791,4.3626 12.3847,4.3296C12.3902,4.2967 12.3862,4.2629 12.3732,4.2323C12.3601,4.2016 12.3385,4.1755 12.3112,4.1572C12.2838,4.1389 12.2517,4.1291 12.219,4.1291H5.353C5.307,4.1292 5.2614,4.1389 5.2192,4.1577C5.177,4.1764 5.1391,4.2038 5.1077,4.2381L3.6585,5.8217C3.6362,5.8461 3.6213,5.8766 3.6157,5.9095C3.6102,5.9424 3.6141,5.9762 3.6272,6.0068C3.6402,6.0374 3.6617,6.0635 3.689,6.0819C3.7163,6.1002 3.7483,6.11 3.7811,6.1101Z"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M8,0L8,0A8,8 0,0 1,16 8L16,8A8,8 0,0 1,8 16L8,16A8,8 0,0 1,0 8L0,8A8,8 0,0 1,8 0z"/>
<path android:pathData="M12.3419,10.4366L10.8935,12.0202C10.862,12.0546 10.8239,12.082 10.7816,12.1008C10.7392,12.1195 10.6936,12.1292 10.6474,12.1292H3.7811C3.7483,12.1292 3.7163,12.1194 3.6889,12.1011C3.6615,12.0828 3.6399,12.0567 3.6268,12.0261C3.6138,11.9954 3.6098,11.9616 3.6154,11.9287C3.6209,11.8957 3.6358,11.8652 3.6581,11.8408L5.1077,10.2572C5.1391,10.2228 5.177,10.1955 5.2192,10.1767C5.2614,10.158 5.307,10.1483 5.353,10.1482H12.219C12.2517,10.1482 12.2838,10.158 12.3112,10.1763C12.3385,10.1946 12.3601,10.2207 12.3732,10.2513C12.3862,10.2819 12.3902,10.3158 12.3847,10.3487C12.3791,10.3816 12.3643,10.4122 12.3419,10.4366ZM10.8935,7.2476C10.862,7.2132 10.8239,7.1858 10.7816,7.167C10.7392,7.1483 10.6936,7.1386 10.6474,7.1387H3.7811C3.7483,7.1387 3.7163,7.1484 3.6889,7.1667C3.6615,7.1851 3.6399,7.2111 3.6268,7.2418C3.6138,7.2724 3.6098,7.3063 3.6154,7.3392C3.6209,7.3721 3.6358,7.4026 3.6581,7.4271L5.1077,9.0107C5.1391,9.045 5.177,9.0723 5.2192,9.0911C5.2614,9.1098 5.307,9.1195 5.353,9.1196H12.219C12.2517,9.1196 12.2838,9.1099 12.3112,9.0915C12.3385,9.0732 12.3601,9.0472 12.3732,9.0165C12.3862,8.9859 12.3902,8.952 12.3847,8.9191C12.3791,8.8862 12.3643,8.8557 12.3419,8.8312L10.8935,7.2476ZM3.7811,6.1101H10.6474C10.6936,6.1101 10.7392,6.1005 10.7816,6.0817C10.8239,6.063 10.862,6.0356 10.8935,6.0012L12.3419,4.4175C12.3643,4.3931 12.3791,4.3626 12.3847,4.3296C12.3902,4.2967 12.3862,4.2629 12.3732,4.2323C12.3601,4.2016 12.3385,4.1755 12.3112,4.1572C12.2838,4.1389 12.2517,4.1291 12.219,4.1291H5.353C5.307,4.1292 5.2614,4.1389 5.2192,4.1577C5.177,4.1764 5.1391,4.2038 5.1077,4.2381L3.6585,5.8217C3.6362,5.8461 3.6213,5.8766 3.6157,5.9095C3.6102,5.9424 3.6141,5.9762 3.6272,6.0068C3.6402,6.0374 3.6617,6.0635 3.689,6.0819C3.7163,6.1002 3.7483,6.11 3.7811,6.1101Z">
<aapt:attr name="android:fillColor">
<gradient android:endX="11.7097" android:endY="4.18558"
android:startX="4.35356" android:startY="12.3198" android:type="linear">
<item android:color="#FF9945FF" android:offset="0.08"/>
<item android:color="#FF8752F3" android:offset="0.3"/>
<item android:color="#FF5497D5" android:offset="0.5"/>
<item android:color="#FF43B4CA" android:offset="0.6"/>
<item android:color="#FF28E0B9" android:offset="0.72"/>
<item android:color="#FF19FB9B" android:offset="0.97"/>
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -0,0 +1,12 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h16v16h-16z"/>
<path android:fillColor="#F1F1F1" android:pathData="M8,0L8,0A8,8 0,0 1,16 8L16,8A8,8 0,0 1,8 16L8,16A8,8 0,0 1,0 8L0,8A8,8 0,0 1,8 0z"/>
<group>
<clip-path android:pathData="M7.871,2.8386L7.871,2.8386A5.0322,5.0322 0,0 1,12.9032 7.8709L12.9032,7.8709A5.0322,5.0322 0,0 1,7.871 12.9031L7.871,12.9031A5.0322,5.0322 0,0 1,2.8387 7.8709L2.8387,7.8709A5.0322,5.0322 0,0 1,7.871 2.8386z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9897,3.6149C7.4207,3.5991 6.8543,3.6974 6.324,3.9042C5.7937,4.111 5.3102,4.4221 4.9022,4.8189C4.4941,5.2158 4.1698,5.6904 3.9483,6.2148C3.7269,6.7392 3.6128,7.3026 3.6129,7.8718C3.6129,7.9804 3.6171,8.0882 3.6254,8.196C3.637,8.3484 3.6031,8.5007 3.5282,8.6339C3.4533,8.7671 3.3406,8.8752 3.2044,8.9445L2.8387,9.1311V10.0009L3.9156,9.4519L4.2646,9.2741L4.6084,9.0993L10.7771,5.9558L11.4703,5.6027L12.9033,4.8726V4.0028L11.4829,4.7275L10.271,5.3452L4.4189,8.326C4.3992,8.175 4.3894,8.0228 4.3895,7.8706C4.3903,7.2632 4.5498,6.6667 4.8521,6.1399C5.1544,5.6132 5.589,5.1746 6.113,4.8675C6.637,4.5605 7.2321,4.3956 7.8393,4.3893C8.4466,4.3829 9.045,4.5354 9.5753,4.8315L10.2689,4.478L10.3725,4.4251C9.678,3.9211 8.8474,3.6388 7.9897,3.6149ZM12.9033,5.7427L4.9586,9.7878L4.2654,10.1418L2.8387,10.8694V11.7383L4.2553,11.0161L5.4673,10.3984L11.3256,7.4139C11.3453,7.5657 11.3551,7.7187 11.355,7.8718C11.3546,8.4798 11.1952,9.0771 10.8926,9.6045C10.59,10.1319 10.1548,10.5709 9.6301,10.8781C9.1054,11.1853 8.5094,11.3498 7.9015,11.3555C7.2935,11.3612 6.6946,11.2078 6.1642,10.9105L6.1219,10.9331L5.37,11.3164C6.0053,11.7776 6.7557,12.0541 7.5383,12.1155C8.3209,12.177 9.1053,12.0208 9.8048,11.6644C10.5042,11.308 11.0915,10.7651 11.5018,10.0958C11.912,9.4265 12.1293,8.6568 12.1296,7.8718C12.1296,7.7628 12.1254,7.6529 12.117,7.5447C12.1055,7.3924 12.1393,7.24 12.2141,7.1069C12.289,6.9737 12.4015,6.8656 12.5376,6.7962L12.9033,6.6095V5.7427Z"/>
</group>
</group>
</vector>

View file

@ -0,0 +1,12 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h16v16h-16z"/>
<path android:fillColor="#000000" android:pathData="M8,0L8,0A8,8 0,0 1,16 8L16,8A8,8 0,0 1,8 16L8,16A8,8 0,0 1,0 8L0,8A8,8 0,0 1,8 0z"/>
<group>
<clip-path android:pathData="M7.871,2.8386L7.871,2.8386A5.0322,5.0322 0,0 1,12.9032 7.8709L12.9032,7.8709A5.0322,5.0322 0,0 1,7.871 12.9031L7.871,12.9031A5.0322,5.0322 0,0 1,2.8387 7.8709L2.8387,7.8709A5.0322,5.0322 0,0 1,7.871 2.8386z"/>
<path android:fillColor="#ffffff" android:pathData="M7.9897,3.6149C7.4207,3.5991 6.8543,3.6974 6.324,3.9042C5.7937,4.111 5.3102,4.4221 4.9022,4.8189C4.4941,5.2158 4.1698,5.6904 3.9483,6.2148C3.7269,6.7392 3.6128,7.3026 3.6129,7.8718C3.6129,7.9804 3.6171,8.0882 3.6254,8.196C3.637,8.3484 3.6031,8.5007 3.5282,8.6339C3.4533,8.7671 3.3406,8.8752 3.2044,8.9445L2.8387,9.1311V10.0009L3.9156,9.4519L4.2646,9.2741L4.6084,9.0993L10.7771,5.9558L11.4703,5.6027L12.9033,4.8726V4.0028L11.4829,4.7275L10.271,5.3452L4.4189,8.326C4.3992,8.175 4.3894,8.0228 4.3895,7.8706C4.3903,7.2632 4.5498,6.6667 4.8521,6.1399C5.1544,5.6132 5.589,5.1746 6.113,4.8675C6.637,4.5605 7.2321,4.3956 7.8393,4.3893C8.4466,4.3829 9.045,4.5354 9.5753,4.8315L10.2689,4.478L10.3725,4.4251C9.678,3.9211 8.8474,3.6388 7.9897,3.6149ZM12.9033,5.7427L4.9586,9.7878L4.2654,10.1418L2.8387,10.8694V11.7383L4.2553,11.0161L5.4673,10.3984L11.3256,7.4139C11.3453,7.5657 11.3551,7.7187 11.355,7.8718C11.3546,8.4798 11.1952,9.0771 10.8926,9.6045C10.59,10.1319 10.1548,10.5709 9.6301,10.8781C9.1054,11.1853 8.5094,11.3498 7.9015,11.3555C7.2935,11.3612 6.6946,11.2078 6.1642,10.9105L6.1219,10.9331L5.37,11.3164C6.0053,11.7776 6.7557,12.0541 7.5383,12.1155C8.3209,12.177 9.1053,12.0208 9.8048,11.6644C10.5042,11.308 11.0915,10.7651 11.5018,10.0958C11.912,9.4265 12.1293,8.6568 12.1296,7.8718C12.1296,7.7628 12.1254,7.6529 12.117,7.5447C12.1055,7.3924 12.1393,7.24 12.2141,7.1069C12.289,6.9737 12.4015,6.8656 12.5376,6.7962L12.9033,6.6095V5.7427Z"/>
</group>
</group>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F1F1F1" android:pathData="M8,8m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path android:fillColor="#A1A1A4" android:pathData="M8.5216,13C7.8414,13 7.345,12.8281 7.0322,12.4844C6.7425,12.1934 6.5744,11.7939 6.5646,11.3732C6.5611,11.2455 6.5893,11.1189 6.6465,11.006C6.6999,10.9085 6.777,10.8275 6.8699,10.7715C6.9779,10.715 7.097,10.6856 7.2177,10.6856C7.3384,10.6856 7.4574,10.715 7.5654,10.7715C7.6582,10.8273 7.7349,10.9083 7.7875,11.006C7.8464,11.1184 7.8757,11.2451 7.8724,11.3732C7.8784,11.5263 7.8366,11.6772 7.7532,11.8029C7.6838,11.9056 7.5844,11.9818 7.4702,12.0203C7.5819,12.1695 7.74,12.2729 7.917,12.3125C8.1149,12.3728 8.3199,12.4038 8.5261,12.4046C8.799,12.4071 9.0663,12.3233 9.2931,12.164C9.5284,11.9896 9.7038,11.7403 9.7935,11.4531C9.9071,11.1112 9.9626,10.7511 9.9573,10.3891C9.9665,10.0117 9.9058,9.636 9.7786,9.2828C9.6829,9.0003 9.5013,8.7587 9.2618,8.5953C9.0392,8.4475 8.7807,8.3699 8.5173,8.3719C8.2877,8.3923 8.0663,8.4711 7.8724,8.6016L7.3942,8.8531V8.6016L9.5433,5.5769H6.5646V8.7156C6.5562,8.9421 6.6133,9.166 6.7284,9.3578C6.7842,9.4414 6.8595,9.5085 6.9472,9.5525C7.0348,9.5966 7.1319,9.6162 7.2288,9.6093C7.4081,9.6055 7.5814,9.5415 7.7233,9.4265C7.8841,9.3029 8.0241,9.1518 8.1373,8.9796C8.1475,8.9495 8.1663,8.9233 8.1912,8.9047C8.2111,8.8866 8.2366,8.8766 8.263,8.8766C8.3147,8.8815 8.3637,8.9034 8.403,8.939C8.4528,8.9974 8.4798,9.0732 8.4789,9.1515C8.4732,9.2042 8.4642,9.2563 8.4521,9.3078C8.3463,9.5765 8.1689,9.8075 7.9412,9.9732C7.7298,10.1233 7.48,10.2029 7.2247,10.2016C6.5798,10.2016 6.133,10.0682 5.8844,9.8016C5.7566,9.6542 5.6581,9.4815 5.5947,9.2937C5.5312,9.1059 5.5042,8.9067 5.515,8.7078V5.5769H4V4.9937H5.5221V3.664L5.1736,3.2982V3H6.1848L6.5646,3.2063V4.9937L10.4993,4.9812L10.8911,5.3937L8.4784,7.9372C8.6241,7.8757 8.7776,7.8368 8.9341,7.8216C9.2437,7.8358 9.5458,7.9263 9.8159,8.0857C10.1287,8.2417 10.3923,8.4885 10.5755,8.7969C10.7396,9.0579 10.8558,9.3488 10.9181,9.6547C10.9692,9.8959 10.9967,10.142 11,10.3891C11.0016,10.8607 10.8997,11.3264 10.7021,11.75C10.5148,12.1602 10.1995,12.491 9.8085,12.6876C9.4082,12.8945 8.9678,13.0014 8.5216,13Z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show more