Updated on 2026-08-14

This commit is contained in:
Tangem 2022-05-31 16:14:06 +03:00
commit fb7aa02b29
155 changed files with 3893 additions and 2754 deletions

View file

@ -3,11 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
ext.configVar = [
configEnvironment: "CONFIG_ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
]
apply from: '../dependencies.gradle'
android {
compileSdkVersion 31
@ -19,7 +15,8 @@ android {
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
buildFeatures {
compose true
@ -31,8 +28,9 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'String', environmentConfig.environment, '\"prod\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
debug {
debuggable true
@ -42,16 +40,18 @@ android {
firebaseCrashlytics {
mappingFileUploadEnabled false
}
buildConfigField 'String', configVar.configEnvironment, '\"dev\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'true'
buildConfigField 'String', environmentConfig.environment, '\"dev\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'true'
}
debug_beta {
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'String', environmentConfig.environment, '\"prod\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
}
@ -93,7 +93,8 @@ dependencies {
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-77'
implementation 'com.tangem:blockchain:develop-85'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
@ -169,6 +170,8 @@ dependencies {
implementation 'androidx.compose.ui:ui-tooling:1.1.1'
implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0"
implementation "com.github.skydoves:androidveil:1.1.2"
implementation("io.coil-kt:coil:2.0.0-rc01")
implementation("io.coil-kt:coil-compose:2.0.0-rc01")
@ -179,4 +182,4 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
}

@ -1 +1 @@
Subproject commit 64ae04d2086e5a605dd4da3cba4af32a60d46c79
Subproject commit 7f058ec409b4eaaf8688ecd4bf944ef8d58d3564

View file

@ -1,371 +1,428 @@
{
"coins" : [
{
"id" : "matic-token",
"symbol" : "MATIC",
"name" : "Polygon",
"networks" : [
"coins" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
}
]
},
{
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
"id" : "matic-token",
"symbol" : "MATIC",
"name" : "Polygon",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
}
]
},
{
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
}
]
},
{
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
}
]
},
{
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
}
]
},
{
"id" : "Tether USD-USDT",
"symbol" : "USDT",
"name" : "Tether USD",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
}
]
},
{
"id" : "solana",
"symbol" : "SOL",
"name" : "Solana",
"networks" : [
{
"networkId" : "solana/test"
}
]
},
{
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"networks" : [
{
"networkId" : "solana/test",
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
}
]
},
{
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"networks" : [
{
"networkId" : "solana/test",
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
}
]
},
{
"id" : "binancecoin",
"symbol" : "BNB",
"name" : "Binance",
"networks" : [
{
"networkId" : "binancecoin/test"
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test"
}
]
},
{
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"networks" : [
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
}
]
},
{
"networkId" : "binancecoin/test",
"contractAddress" : "HEM-452",
"decimalCount" : 8
}
]
},
{
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"networks" : [
"id" : "tether",
"symbol" : "USDT",
"name" : "Tether USD",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
},
{
"networkId" : "tron/test",
"contractAddress" : "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount" : 6
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
}
]
},
{
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"networks" : [
"id" : "just-gov",
"symbol" : "JST",
"name" : "JUST GOV",
"networks" : [
{
"networkId" : "tron/test",
"contractAddress" : "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
}
]
},
{
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"networks" : [
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
}
]
},
{
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"networks" : [
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"networks" : [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
}
]
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
}
]
},
{
"id" : "ethereum",
"symbol" : "ETH",
"name" : "Ethereum",
"networks" : [
"id" : "solana",
"symbol" : "SOL",
"name" : "Solana",
"networks" : [
{
"networkId" : "solana/test"
}
]
},
{
"networkId" : "ethereum/test"
}
]
},
{
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"networks" : [
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"networks" : [
{
"networkId" : "solana/test",
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
}
]
},
{
"networkId" : "ethereum/test",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
}
]
},
{
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"networks" : [
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"networks" : [
{
"networkId" : "solana/test",
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
}
]
},
{
"networkId" : "ethereum/test",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
}
]
},
{
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"networks" : [
"id" : "binancecoin",
"symbol" : "BNB",
"name" : "Binance",
"networks" : [
{
"networkId" : "binancecoin/test"
},
{
"networkId" : "binance-smart-chain/test"
}
]
},
{
"networkId" : "ethereum/test",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
}
]
},
{
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"networks" : [
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"networks" : [
{
"networkId" : "binancecoin/test",
"contractAddress" : "HEM-452",
"decimalCount" : 8
}
]
},
{
"networkId" : "ethereum/test",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
}
]
},
{
"id" : "avalanche-2",
"symbol" : "AVAX",
"name" : "Avalanche",
"networks" : [
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"networks" : [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
}
]
},
{
"networkId" : "avalanche/test"
}
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"networks" : [
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"networks" : [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
}
]
},
{
"networkId" : "avalanche/test",
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
}
]
},
{
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"networks" : [
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"networks" : [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
}
]
},
{
"networkId" : "avalanche/test",
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
}
]
},
{
"id" : "fantom",
"symbol" : "FTM",
"name" : "Fantom",
"networks" : [
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"networks" : [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
}
]
},
{
"networkId" : "fantom/test"
}
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"networks" : [
"id" : "ethereum",
"symbol" : "ETH",
"name" : "Ethereum",
"networks" : [
{
"networkId" : "ethereum/test"
}
]
},
{
"networkId" : "fantom/test",
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"networks" : [
"id" : "ethereum-classic",
"symbol" : "ETC",
"name" : "Ethereum Classic",
"networks" : [
{
"networkId" : "ethereum-classic/test"
}
]
},
{
"networkId" : "fantom/test",
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
}
]
},
{
"id" : "bitcoin",
"symbol" : "BTC",
"name" : "Bitcoin",
"networks" : [
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"networks" : [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
}
]
},
{
"networkId" : "bitcoin/test"
}
]
},
{
"id" : "stellar",
"symbol" : "XLM",
"name" : "Stellar",
"networks" : [
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"networks" : [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
}
]
},
{
"networkId" : "stellar/test"
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"networks" : [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
}
]
},
{
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"networks" : [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
}
]
},
{
"id" : "avalanche-2",
"symbol" : "AVAX",
"name" : "Avalanche",
"networks" : [
{
"networkId" : "avalanche/test"
}
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"networks" : [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
}
]
},
{
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"networks" : [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
}
]
},
{
"id" : "fantom",
"symbol" : "FTM",
"name" : "Fantom",
"networks" : [
{
"networkId" : "fantom/test"
}
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"networks" : [
{
"networkId" : "fantom/test",
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
}
]
},
{
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"networks" : [
{
"networkId" : "fantom/test",
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
}
]
},
{
"id" : "bitcoin",
"symbol" : "BTC",
"name" : "Bitcoin",
"networks" : [
{
"networkId" : "bitcoin/test"
}
]
},
{
"id" : "tron",
"symbol" : "TRX",
"name" : "Tron",
"networks" : [
{
"networkId" : "tron/test"
}
]
},
{
"id" : "arbitrum-one",
"symbol" : "ETH",
"name" : "Arbitrum",
"networks" : [
{
"networkId" : "arbitrum-one/test"
}
]
},
{
"id" : "stellar",
"symbol" : "XLM",
"name" : "Stellar",
"networks" : [
{
"networkId" : "stellar/test"
}
]
}
]
}
],
"total" : 0,
"imageHost" : "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
],
"total" : 0,
"imageHost" : "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
}

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,7 @@ import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.images.PicassoHelper
@ -46,6 +47,7 @@ class TapApplication : Application() {
override fun onCreate() {
super.onCreate()
DomainLayer.init()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
@ -60,7 +62,9 @@ class TapApplication : Application() {
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
currenciesRepository = CurrenciesRepository(this)
currenciesRepository = CurrenciesRepository(
this, store.state.domainNetworks.tangemTechService
)
walletConnectRepository = WalletConnectRepository(this)
initFeedbackManager()

View file

@ -5,7 +5,12 @@ import android.content.Context
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.SimpleAlertDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
@ -14,8 +19,13 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBack
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog
import com.tangem.tap.features.wallet.ui.wallet.CurrencySelectionDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -54,7 +64,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog ->
CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
@ -96,6 +107,14 @@ class DialogManager : StoreSubscriber<GlobalState> {
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)
is WalletDialog.CurrencySelectionDialog ->
CurrencySelectionDialog.create(state.dialog, context)
is WalletDialog.ChooseTradeActionDialog ->
ChooseTradeActionBottomSheetDialog(context)
is WalletDialog.SelectAmountToSendDialog ->
AmountToSendBottomSheetDialog(context, state.dialog)
is WalletDialog.SignedHashesMultiWalletDialog ->
SignedHashesWarningDialog.create(context)
else -> null
}
dialog?.show()

View file

@ -16,6 +16,7 @@ fun BlockchainSpinner(
isEnabled: Boolean = true,
textFieldConverter: (Blockchain) -> String,
dropdownItemView: @Composable ((Blockchain) -> Unit)? = null,
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
onItemSelected: (Blockchain) -> Unit,
) {
OutlinedSpinner(
@ -26,6 +27,7 @@ fun BlockchainSpinner(
textFieldConverter = textFieldConverter,
dropdownItemView = dropdownItemView,
isEnabled = isEnabled,
onItemSelected = onItemSelected
onItemSelected = onItemSelected,
closePopupTrigger = closePopupTrigger,
)
}

View file

@ -1,11 +1,14 @@
package com.tangem.tap.common.compose
import android.os.Handler
import android.os.Looper
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.os.postDelayed
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.form.Field
@ -25,7 +28,8 @@ fun <T> OutlinedSpinner(
textFieldConverter: (T) -> String = { it.toString() },
dropdownItemView: @Composable ((T) -> Unit)? = null,
isEnabled: Boolean = true,
onClose: VoidCallback = {}
onClose: VoidCallback = {},
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
) {
val rIsExpanded = remember { mutableStateOf(false) }
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
@ -43,6 +47,13 @@ fun <T> OutlinedSpinner(
onClose()
}
closePopupTrigger.close = {
onDismissRequest()
Handler(Looper.getMainLooper()).postDelayed(100) {
closePopupTrigger.onCloseComplete()
}
}
ExposedDropdownMenuBox(
expanded = rIsExpanded.value,
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
@ -57,16 +68,17 @@ fun <T> OutlinedSpinner(
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
)
if (!isEnabled) return@ExposedDropdownMenuBox
ExposedDropdownMenu(
expanded = rIsExpanded.value,
onDismissRequest = onDismissRequest,
) {
itemList.forEach { item ->
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
when (dropdownItemView) {
null -> Text(textFieldConverter(item))
else -> dropdownItemView(item)
if (isEnabled) {
ExposedDropdownMenu(
expanded = rIsExpanded.value,
onDismissRequest = onDismissRequest,
) {
itemList.forEach { item ->
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
when (dropdownItemView) {
null -> Text(textFieldConverter(item))
else -> dropdownItemView(item)
}
}
}
}
@ -74,6 +86,12 @@ fun <T> OutlinedSpinner(
}
}
class ClosePopupTrigger {
var close: () -> Unit = {}
var onCloseComplete: () -> Unit = {}
var isTriggered = false
}
@Preview
@Composable
fun TestSpinnerPreview() {

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
@Composable
fun LazyListState.OnBottomReached(loadMoreThreshold: Int, loadMore: () -> Unit) {
require(loadMoreThreshold >= 0)
val shouldLoadMore by remember {
derivedStateOf {
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
?: return@derivedStateOf false
lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - loadMoreThreshold
}
}
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) loadMore()
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.common.entities
data class FiatCurrency(
val code: String,
val name: String,
val symbol: String,
) {
val displayName: String
get() = "${this.name} (${this.code}) - ${this.symbol}"
companion object {
val Default = FiatCurrency(
symbol = "$",
code = "USD",
name = "US Dollar",
)
}
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.common.entities
/**
[REDACTED_AUTHOR]
*/
class TapCurrency {
companion object{
const val DEFAULT_FIAT_CURRENCY = "USD"
}
}

View file

@ -13,6 +13,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_round
Blockchain.Litecoin -> R.drawable.ic_litecoin_round
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth_round
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> R.drawable.ic_eth_round
Blockchain.RSK -> R.drawable.ic_rsk_round
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_round
Blockchain.Tezos -> R.drawable.ic_tezos_round
@ -24,6 +25,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_round
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round
Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_round
else -> R.drawable.ic_tangem_logo
}
}
@ -36,6 +38,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_no_color
Blockchain.Litecoin -> R.drawable.ic_litecoin_no_color
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth_no_color
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet, -> R.drawable.ic_eth_no_color
Blockchain.RSK -> R.drawable.ic_rsk_no_color
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_no_color
Blockchain.Tezos -> R.drawable.ic_tezos_no_color
@ -47,6 +50,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color
Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -25,6 +25,7 @@ import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.R
import timber.log.Timber
private class Navigation
@ -52,12 +53,18 @@ fun FragmentActivity.openFragment(
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag)
try {
this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag)
} catch (e: IllegalStateException) {
Timber.e(e)
}
}
fun FragmentActivity.getPreviousScreen(): AppScreen? {
val indexOfLastFragment = this.supportFragmentManager.backStackEntryCount - 1
val tag = this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount)
this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
else null
return tag?.let { AppScreen.valueOf(tag) }
}

View file

@ -1,8 +1,10 @@
package com.tangem.tap.common.extensions
import android.text.Spanned
import android.text.SpannedString
import android.text.style.RelativeSizeSpan
import androidx.core.text.buildSpannedString
import com.tangem.common.extensions.isZero
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.redux.global.FiatCurrencyName
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
@ -37,10 +39,23 @@ fun BigDecimal.toFormattedCurrencyString(
return "$formattedAmount $currency"
}
fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrencyName): String {
fun BigDecimal.toFiatRateString(
fiatCurrencyName: String
): String {
val value = this
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
return "$value $fiatCurrencyName"
}
fun BigDecimal.toFiatString(
rateValue: BigDecimal,
fiatCurrencyName: String,
formatWithSpaces: Boolean = false
): String {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.HALF_UP)
return "≈ ${fiatCurrencyName} $fiatValue"
return fiatValue.toFormattedFiatValue(fiatCurrencyName, formatWithSpaces)
}
fun BigDecimal.toFiatValue(rateValue: BigDecimal): BigDecimal {
@ -48,12 +63,14 @@ fun BigDecimal.toFiatValue(rateValue: BigDecimal): BigDecimal {
return fiatValue.setScale(2, RoundingMode.HALF_UP)
}
fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: FiatCurrencyName): String {
return "≈ ${fiatCurrencyName} $this"
fun BigDecimal.toFormattedFiatValue(
fiatCurrencyName: String,
formatWithSpaces: Boolean = false
): String {
val fiatValue = if (formatWithSpaces) this.formatWithSpaces() else this
return "${fiatValue} $fiatCurrencyName"
}
fun CurrenciesResponse.Currency.toFormattedString(): String = "${this.name} (${this.code}) - ${this.unit}"
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
// 0.00 -> 0.00
@ -87,4 +104,54 @@ fun BigDecimal.isGreaterThanOrEqual(value: BigDecimal): Boolean {
fun BigDecimal.isLessThanOrEqual(value: BigDecimal): Boolean {
val compareResult = this.compareTo(value)
return compareResult == -1 || compareResult == 0
}
fun BigDecimal.formatAmountAsSpannedString(
currencySymbol: String,
reminderPartSizeProportion: Float = 0.7f
): SpannedString {
val amount = this
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
val integer = amount.substringBefore('.')
val reminder = amount.substringAfter('.')
return buildSpannedString {
append(integer)
append('.')
append(
"$reminder $currencySymbol",
RelativeSizeSpan(reminderPartSizeProportion),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun BigDecimal.formatWithSpaces(): String {
val str = this.toString()
var integerStr = str.substringBefore('.')
val reminderStr = str.substringAfter('.')
val packets = arrayListOf<String>()
var index: Int = integerStr.length
while (0 < index) {
if (index <= 3) {
packets.add(0, integerStr)
break
}
index -= 3
packets.add(integerStr.substring(startIndex = index))
integerStr = integerStr.substring(startIndex = 0, endIndex = index)
}
return buildString {
packets.forEachIndexed { index, packet ->
append(packet)
if (index != packets.lastIndex) append(' ')
}
if (reminderStr.isNotBlank()) {
append('.')
append(reminderStr)
}
}
}

View file

@ -16,6 +16,7 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
import com.tangem.common.extensions.VoidCallback
@ -187,4 +188,29 @@ fun Context.safeStartActivity(
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, formatArgs)
}
}
fun View.animateVisibility(
show: Boolean,
durationMillis: Long = SHORT_ANIMATION_DURATION,
hiddenVisibility: Int = View.GONE
) {
if (show) {
this.animate()
.alpha(1f)
.setDuration(durationMillis)
.withStartAction {
this.alpha = 0f
this.isVisible = true
}
} else {
this.animate()
.alpha(0f)
.setDuration(durationMillis)
.withStartAction {
this.visibility = hiddenVisibility
}
}
}
private const val SHORT_ANIMATION_DURATION = 80L

View file

@ -57,7 +57,7 @@ fun WalletManager?.getToUpUrl(): String? {
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
cryptoCurrencyName = wallet.blockchain.currency,
fiatCurrency = globalState.appCurrency,
fiatCurrencyName = globalState.appCurrency.code,
walletAddress = defaultAddress,
)
}

View file

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

View file

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

View file

@ -12,7 +12,12 @@ interface StateDialog
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkDialogRes(val headerId: Int, val messageId: Int, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkDialogRes(
val headerId: Int,
val messageId: Int,
val onOk: VoidCallback? = null
) : AppDialog()
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,

View file

@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.*
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
@ -54,9 +55,9 @@ sealed class GlobalAction : Action {
data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrency) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
data class Success(val appCurrency: FiatCurrency) : GlobalAction()
}
data class UpdateWalletSignedHashes(

View file

@ -24,7 +24,7 @@ class GlobalMiddleware {
}
}
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
private val globalMiddlewareHandler: Middleware<AppState> = { _, appState ->
{ nextDispatch ->
{ action ->
when (action) {
@ -45,7 +45,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.getAppCurrency()
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
))
}
is GlobalAction.HideWarningMessage -> {

View file

@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
@ -22,7 +22,7 @@ data class GlobalState(
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val currencyExchangeManager: CurrencyExchangeManager? = null,
@ -40,7 +40,6 @@ data class AndroidResources(
)
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
data class OnboardingState(
val onboardingStarted: Boolean = false,

View file

@ -15,7 +15,7 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
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.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
@ -34,17 +34,17 @@ class TapWalletManager {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler = ThrottlerWithValues<Blockchain, Result<Wallet>>(10000)
private val walletManagersThrottler = ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
suspend fun loadWalletData(walletManager: WalletManager) {
val blockchain = walletManager.wallet.blockchain
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val result = if (walletManagersThrottler.isStillThrottled(blockchain)) {
walletManagersThrottler.geValue(blockchain)!!
val result = if (walletManagersThrottler.isStillThrottled(blockchainNetwork)) {
walletManagersThrottler.geValue(blockchainNetwork)!!
} else {
val safeUpdateResult = walletManager.safeUpdate()
walletManagersThrottler.updateThrottlingTo(blockchain)
walletManagersThrottler.setValue(blockchain, safeUpdateResult)
walletManagersThrottler.updateThrottlingTo(blockchainNetwork)
walletManagersThrottler.setValue(blockchainNetwork, safeUpdateResult)
safeUpdateResult
}
when (result) {

View file

@ -9,8 +9,8 @@ interface Loader<T> {
fun load(onComplete: (T) -> Unit)
companion object {
const val featuresName = "features_${BuildConfig.CONFIG_ENVIRONMENT}"
const val configValuesName = "tangem-app-config/config_${BuildConfig.CONFIG_ENVIRONMENT}"
const val warnings = "warnings_${BuildConfig.CONFIG_ENVIRONMENT}"
const val featuresName = "features_${BuildConfig.ENVIRONMENT}"
const val configValuesName = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}"
const val warnings = "warnings_${BuildConfig.ENVIRONMENT}"
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.extensions.isAboveZero
fun Map<AmountType, Amount>.toSendableAmounts(): List<Amount> {
return this.toList().unzip().second
.filter { it.type != AmountType.Reserve }
.filter { it.isAboveZero() }
}

View file

@ -10,7 +10,7 @@ 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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.Currency
fun WalletManagerFactory.makeWalletManagerForApp(

View file

@ -28,9 +28,11 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import kotlinx.coroutines.launch
class ScanProductTask(
val card: Card? = null,
@ -192,54 +194,56 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
primaryCard = primaryCard
scope.launch {
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
primaryCard = primaryCard
)
)
)
)
return
}
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard
)
callback(CompletionResult.Success(response))
return@launch
}
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard
)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card))
BlockchainNetwork(Blockchain.Ethereum, card)
)
}
if (card.settings.isHDWalletAllowed) {
blockchainsToDerive.addAll(
listOf(
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.Binance, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card)
)
)
@ -261,7 +265,7 @@ private class ScanWalletProcessor(
return blockchainsToDerive.distinct()
}
private fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
private suspend fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
val blockchains = getBlockchainsToDerive(card)
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()

View file

@ -2,26 +2,32 @@ package com.tangem.tap.domain.tokens
import android.app.Application
import android.content.Context
import com.squareup.moshi.Json
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.domain.common.TapWorkarounds.derivationStyle
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
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.setCustomIconUrl
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.ObsoleteTokenDao
import com.tangem.tap.domain.tokens.models.TokenDao
import com.tangem.tap.features.demo.DemoHelper
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.util.*
class CurrenciesRepository(val context: Application) {
class CurrenciesRepository(
private val context: Application,
private val tangemNetworkService: TangemTechService
) {
private val moshi = MoshiConverter.defaultMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
@ -41,7 +47,7 @@ class CurrenciesRepository(val context: Application) {
fun saveUpdatedCurrency(cardId: String, blockchainNetwork: BlockchainNetwork) {
var changed = false
val currencies = loadSavedCurrencies(cardId).map {
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
if (it == blockchainNetwork) {
changed = true
blockchainNetwork
@ -54,7 +60,7 @@ class CurrenciesRepository(val context: Application) {
}
fun removeToken(cardId: String, token: Token, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrencies(cardId).map {
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
if (it == blockchainNetwork) {
it.copy(tokens = it.tokens.filterNot { it == token })
} else {
@ -65,7 +71,8 @@ class CurrenciesRepository(val context: Application) {
}
fun removeBlockchain(cardId: String, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrencies(cardId).filterNot { it == blockchainNetwork }
val currencies = loadSavedCurrenciesWithoutMigration(cardId)
.filterNot { it == blockchainNetwork }
saveCurrencies(cardId, currencies)
}
@ -98,12 +105,12 @@ class CurrenciesRepository(val context: Application) {
}
}
fun loadSavedCurrencies(
suspend fun loadSavedCurrencies(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies(cardId)
return loadDemoCurrencies()
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
@ -113,7 +120,21 @@ class CurrenciesRepository(val context: Application) {
}
}
private fun loadDemoCurrencies(cardId: String): List<BlockchainNetwork> {
fun loadSavedCurrenciesWithoutMigration(
cardId: String,
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies()
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
private fun loadDemoCurrencies(): List<BlockchainNetwork> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
@ -123,7 +144,7 @@ class CurrenciesRepository(val context: Application) {
}
}
private fun tryToLoadPreviousFormatAndMigrate(
private suspend fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
@ -137,12 +158,12 @@ class CurrenciesRepository(val context: Application) {
}
}
private fun loadSavedCurrenciesOldWay(
private suspend fun loadSavedCurrenciesOldWay(
cardId: String, isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val currencies = getSupportedTokens()
val ids = getTokensIds(tokens)
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
@ -152,10 +173,7 @@ class CurrenciesRepository(val context: Application) {
.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)
token.copy(id = ids[token.contractAddress])
}
)
}
@ -163,6 +181,21 @@ class CurrenciesRepository(val context: Application) {
return blockchainNetworks
}
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
tokens.map {
async {
tangemNetworkService.getTokens(
it.contractAddress,
it.blockchainDao.toBlockchain().toNetworkId()
)
}
}.map { it.await() }
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
.mapIndexedNotNull { index, id ->
if (id == null) null else tokens[index].contractAddress to id
}.toMap()
}
fun saveCurrencies(cardId: String, currencies: List<BlockchainNetwork>) {
val json = blockchainNetworkAdapter.toJson(currencies)
context.rewriteFile(json, getFileNameForBlockchains(cardId))
@ -177,9 +210,8 @@ class CurrenciesRepository(val context: Application) {
}
}
fun getSupportedTokens(isTestNet: Boolean = false): List<Currency> {
val fileName = if (isTestNet) "testnet_tokens" else "tokens"
val json = context.assets.readJsonFileToString(fileName)
fun getTestnetCoins(): List<Currency> {
val json = context.assets.readJsonFileToString(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
@ -232,6 +264,7 @@ class CurrenciesRepository(val context: Application) {
companion object {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
fun getFileNameForBlockchains(cardId: String): String =
@ -245,145 +278,3 @@ fun Blockchain.getTokensName(): String {
else -> this.fullName
}
}
@JsonClass(generateAdapter = true)
data class TokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
@Json(name = "blockchain")
val blockchainDao: BlockchainDao,
val customIconUrl: String? = null,
val type: String? = null
) {
fun toToken(): Token {
return Token(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimals = decimalCount,
).apply {
customIconUrl?.let { this.setCustomIconUrl(it) }
}
}
}
@JsonClass(generateAdapter = true)
data class BlockchainDao(
@Json(name = "key")
val name: String,
@Json(name = "testnet")
val isTestNet: Boolean
) {
fun toBlockchain(): Blockchain {
val blockchain = Blockchain.values().find { it.name.lowercase() == name.lowercase() }
?: throw Exception("Invalid BlockchainDao")
return if (!isTestNet) blockchain else blockchain.getTestnetVersion()
?: throw Exception("Invalid BlockchainDao")
}
companion object {
fun fromBlockchain(blockchain: Blockchain): BlockchainDao {
val name = blockchain.name.removeSuffix("Testnet").lowercase()
return BlockchainDao(name, blockchain.isTestnet())
}
}
}
@JsonClass(generateAdapter = true)
data class ObsoleteTokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
val customIconUrl: String?,
) {
fun toTokenDao(blockchain: Blockchain): TokenDao {
return TokenDao(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimalCount = decimalCount,
blockchainDao = BlockchainDao.fromBlockchain(blockchain),
customIconUrl = customIconUrl
)
}
}
//@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

@ -3,6 +3,7 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(
@ -44,10 +45,20 @@ data class Currency(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id),
iconUrl = getIconUrl(currency.id, null),
contracts = currency.networks?.toContracts() ?: emptyList()
)
}
fun fromCoinResponse(currency: CoinsResponse.Coin, imageHost: String?): Currency {
return Currency(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id, imageHost),
contracts = currency.networks.mapNotNull { Contract.fromNetwork(it, imageHost) }
)
}
}
}
@ -67,12 +78,26 @@ data class Contract(
blockchain = blockchain,
address = contract.contractAddress,
decimalCount = contract.decimalCount,
iconUrl = getIconUrl(contract.networkId)
iconUrl = getIconUrl(contract.networkId, null)
)
}
fun fromNetwork(contract: CoinsResponse.Coin.Network, imageHost: String?): Contract? {
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: return null
return Contract(
networkId = contract.networkId,
blockchain = blockchain,
address = contract.contractAddress,
decimalCount = contract.decimalCount?.toInt(),
iconUrl = getIconUrl(contract.networkId, imageHost)
)
}
}
}
fun getIconUrl(id: String): String {
return "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/$id.png"
}
fun getIconUrl(id: String, imageHost: String? = null): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}
private const val DEFAULT_IMAGE_HOST =
"https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"

View file

@ -0,0 +1,71 @@
package com.tangem.tap.domain.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getListOfCoins
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.network.api.tangemTech.TangemTechService
class LoadAvailableCoinsService(
private val networkService: TangemTechService,
private val currenciesRepository: CurrenciesRepository
) {
suspend fun getSupportedTokens(
isTestNet: Boolean = false,
supportedBlockchains: List<Blockchain>,
page: Int,
searchInput: String? = null
): Result<LoadedCoins> {
if (isTestNet) {
return Result.Success(
LoadedCoins(
currencies = currenciesRepository.getTestnetCoins(),
moreAvailable = false,
)
)
}
val offset = page * LOAD_PER_PAGE
val result = loadCoins(supportedBlockchains, offset, searchInput)
return when (result) {
is Result.Success -> {
val data = result.data
Result.Success(
LoadedCoins(
currencies = data.coins.map { Currency.fromCoinResponse(it, data.imageHost) },
moreAvailable = data.total > offset + LOAD_PER_PAGE,
)
)
}
is Result.Failure -> {
Result.Failure(result.error)
}
}
}
private suspend fun loadCoins(
supportedBlockchains: List<Blockchain>,
offset: Int,
searchInput: String? = null
): Result<CoinsResponse> {
val networkIds = supportedBlockchains.toSet().map { it.toNetworkId() }
return networkService.getListOfCoins(
networkIds = networkIds,
offset = offset,
limit = LOAD_PER_PAGE,
searchText = searchInput
)
}
companion object {
const val LOAD_PER_PAGE = 100
}
}
data class LoadedCoins(
val currencies: List<Currency>,
val moreAvailable: Boolean,
)

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
@Deprecated("The class is used only for migration from older versions of the app")
@JsonClass(generateAdapter = true)
data class BlockchainDao(
@Json(name = "key")
val name: String,
@Json(name = "testnet")
val isTestNet: Boolean
) {
@Deprecated("The method is used only for migration from older versions of the app")
fun toBlockchain(): Blockchain {
val blockchain = Blockchain.values().find { it.name.lowercase() == name.lowercase() }
?: throw Exception("Invalid BlockchainDao")
return if (!isTestNet) blockchain else blockchain.getTestnetVersion()
?: throw Exception("Invalid BlockchainDao")
}
companion object {
@Deprecated("The method is used only for migration from older versions of the app")
fun fromBlockchain(blockchain: Blockchain): BlockchainDao {
val name = blockchain.name.removeSuffix("Testnet").lowercase()
return BlockchainDao(name, blockchain.isTestnet())
}
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateHashCode
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@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 = calculateHashCode(
blockchain.hashCode(), derivationPath?.hashCode() ?: 0
)
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,24 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
@JsonClass(generateAdapter = true)
data class ObsoleteTokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
val customIconUrl: String?,
) {
fun toTokenDao(blockchain: Blockchain): TokenDao {
return TokenDao(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimalCount = decimalCount,
blockchainDao = BlockchainDao.fromBlockchain(blockchain),
customIconUrl = customIconUrl
)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.extensions.setCustomIconUrl
@JsonClass(generateAdapter = true)
data class TokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
@Json(name = "blockchain")
val blockchainDao: BlockchainDao,
val customIconUrl: String? = null,
val type: String? = null
) {
fun toToken(): Token {
return Token(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimals = decimalCount,
).apply {
customIconUrl?.let { this.setCustomIconUrl(it) }
}
}
}

View file

@ -4,11 +4,8 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.wallet.R
import org.rekotlin.Action
@ -19,9 +16,6 @@ sealed class DetailsAction : Action {
val scanResponse: ScanResponse,
val wallets: List<Wallet>,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
val tangemTechService: TangemTechService,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -47,13 +41,6 @@ sealed class DetailsAction : Action {
object CreateBackup : DetailsAction()
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<CurrenciesResponse.Currency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()
}
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()

View file

@ -3,9 +3,7 @@ package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.dispatchNotification
@ -14,11 +12,14 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -26,15 +27,12 @@ import org.rekotlin.Middleware
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val appCurrencyMiddleware = AppCurrencyMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
val detailsMiddleware: Middleware<AppState> = { dispatch, state ->
val detailsMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is DetailsAction.PrepareScreen -> prepareData(action)
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
@ -67,29 +65,7 @@ class DetailsMiddleware {
}
}
private fun prepareData(action: DetailsAction.PrepareScreen) {
val fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(storedFiatCurrencies))
}
scope.launch {
val tangemTechService = action.tangemTechService
when (val result = tangemTechService.currencies()) {
is Result.Success -> {
val currenciesList = result.data.currencies
if (currenciesList.isNotEmpty() && currenciesList.toSet() != storedFiatCurrencies.toSet()) {
fiatCurrenciesPrefStorage.save(currenciesList)
dispatchOnMain(DetailsAction.AppCurrencyAction.SetCurrencies(currenciesList))
}
}
is Result.Failure -> {}
}
}
}
class EraseWalletMiddleware() {
class EraseWalletMiddleware {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.ResetToFactory.Proceed -> {
@ -100,6 +76,8 @@ class DetailsMiddleware {
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotAllowedByCard)
EraseWalletState.NotEmpty ->
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotEmpty)
else -> { /* no-op */
}
}
}
is DetailsAction.ResetToFactory.Cancel -> {
@ -128,18 +106,7 @@ class DetailsMiddleware {
}
}
}
}
}
}
class AppCurrencyMiddleware {
fun handle(action: DetailsAction.AppCurrencyAction) {
when (action) {
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
store.state.globalState.tapWalletManager.rates.clear()
preferencesStorage.saveAppCurrency(action.fiatCurrencyName)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName))
store.dispatch(WalletAction.LoadFiatRate())
else -> { /* no-op */
}
}
}
@ -211,18 +178,22 @@ class DetailsMiddleware {
actionToLog = Analytics.ActionToLog.ChangeSecOptions,
parameters = mapOf(
AnalyticsParam.NEW_SECURITY_OPTION to
(selectedOption?.name ?: "")
(selectedOption?.name ?: "")
),
card = store.state.detailsState.scanResponse?.card
)
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
else -> { /* no-op */
}
}
}
}
}
else -> { /* no-op */
}
}
}
}
}
}

View file

@ -7,8 +7,7 @@ import com.tangem.domain.common.isTangemTwin
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import org.rekotlin.Action
import java.util.*
@ -29,9 +28,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.AppCurrencyAction -> {
handleAppCurrencyAction(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
@ -48,10 +44,6 @@ private fun handlePrepareScreen(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName,
showAppCurrencyDialog = false,
),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
)
@ -69,9 +61,7 @@ private fun handleEraseWallet(
(card?.isWalletDataSupported == true &&
(!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup()))
val notEmpty = state.wallets.any {
it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty()
}
val notEmpty = state.wallets.any { it.hasSendableAmountsOrPendingTransactions() }
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
notEmpty -> EraseWalletState.NotEmpty
@ -93,30 +83,6 @@ private fun handleEraseWallet(
}
}
private fun handleAppCurrencyAction(
action: DetailsAction.AppCurrencyAction, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppCurrencyAction.SetCurrencies -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(fiatCurrencies = action.currencies))
}
DetailsAction.AppCurrencyAction.ChooseAppCurrency -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(showAppCurrencyDialog = true))
}
DetailsAction.AppCurrencyAction.Cancel -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(showAppCurrencyDialog = false))
}
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
state.copy(
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName, showAppCurrencyDialog = false
)
)
}
else -> state
}
}
private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState,
): DetailsState {

View file

@ -3,10 +3,7 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.store
import org.rekotlin.StateType
@ -17,7 +14,6 @@ data class DetailsState(
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
@ -50,10 +46,4 @@ data class SecurityScreenState(
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<CurrenciesResponse.Currency>? = null,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -16,15 +16,17 @@ 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.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.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.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -257,14 +259,16 @@ class WalletConnectMiddleware {
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {
currenciesRepository.saveUpdatedCurrency(
card.cardId,
BlockchainNetwork.fromWalletManager(walletManager)
)
scope.launch {
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {
currenciesRepository.saveUpdatedCurrency(
card.cardId,
BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
}
return walletManager

View file

@ -1,44 +0,0 @@
package com.tangem.tap.features.details.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.store
import com.tangem.wallet.R
class CurrencySelectionDialog {
var dialog: AlertDialog? = null
fun show(currenciesList: List<CurrenciesResponse.Currency>, currentAppCurrency: FiatCurrencyName, context: Context) {
if (dialog == null) {
val currenciesToShow = currenciesList.map { it.toFormattedString() }.toTypedArray()
var currentSelection = currenciesList.indexOfFirst { it.code == currentAppCurrency }
dialog = AlertDialog.Builder(context)
.setTitle(context.getString(R.string.details_row_title_currency))
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ ->
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
}
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
val selectedCurrency = currenciesList[currentSelection]
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.code))
}
.setOnDismissListener {
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
}
.setSingleChoiceItems(currenciesToShow, currentSelection) { _, which ->
currentSelection = which
}.show()
}
}
fun clear() {
dialog = null
}
}

View file

@ -24,7 +24,6 @@ import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private var currencySelectionDialog = CurrencySelectionDialog()
private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
@ -115,11 +114,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(DetailsAction.CreateBackup)
}
tvAppCurrency.text = state.appCurrencyState.fiatCurrencyName
tvAppCurrencyTitle.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
@ -140,17 +134,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
null -> null
}
currentSecurity?.let { tvSecurity.text = getString(it) }
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
currencySelectionDialog.show(
state.appCurrencyState.fiatCurrencies,
state.appCurrencyState.fiatCurrencyName,
requireContext()
)
} else {
currencySelectionDialog.clear()
}
}
}

View file

@ -8,7 +8,11 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf

View file

@ -24,7 +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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*

View file

@ -1,10 +1,13 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.FeePrecision
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.SendState
@ -58,7 +61,8 @@ class FeeReducer : SendInternalReducer {
selectedFeeType = feeType,
feeList = fees,
currentFee = currentFee,
error = null
error = null,
feePrecision = getFeePrecision(sendState)
)
} else {
val feeType = getCurrentFeeType(state)
@ -68,7 +72,8 @@ class FeeReducer : SendInternalReducer {
selectedFeeType = feeType,
feeList = fees,
currentFee = currentFee,
error = null
error = null,
feePrecision = getFeePrecision(sendState)
)
}
}
@ -88,8 +93,6 @@ class FeeReducer : SendInternalReducer {
if (list == null || list.isEmpty()) return null
return if (list.size == 1) {
if (!list[0].isAboveZero()) return null
list[0]
} else {
when (feeType) {
@ -101,6 +104,17 @@ class FeeReducer : SendInternalReducer {
}
}
private fun getFeePrecision(sendState: SendState): FeePrecision {
val blockchain = sendState.walletManager?.wallet?.blockchain
return if (blockchain?.fullNameWithoutTestnet == Blockchain.Tron.fullName &&
sendState.amountState.typeOfAmount is AmountType.Token) {
FeePrecision.CAN_BE_LOWER
} else {
FeePrecision.PRECISE
}
}
private fun getCurrentFeeType(state: FeeState): FeeType {
return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType
}

View file

@ -160,14 +160,14 @@ class ReceiptReducer : SendInternalReducer {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = EMPTY,
symbols = symbols
)
@ -177,7 +177,7 @@ class ReceiptReducer : SendInternalReducer {
private fun determineSymbols(wallet: Wallet, amountType: AmountType): ReceiptSymbols {
return ReceiptSymbols(
fiat = store.state.globalState.appCurrency,
fiat = store.state.globalState.appCurrency.code,
crypto = wallet.blockchain.currency,
token = when (amountType) {
is AmountType.Token -> amountType.token.symbol
@ -208,4 +208,7 @@ class ReceiptReducer : SendInternalReducer {
else -> EMPTY
}
}
private fun String.addPrecisionSign(): String =
("${feeState.feePrecision.symbol} $this").trim()
}

View file

@ -11,6 +11,10 @@ enum class FeeType {
SINGLE, LOW, NORMAL, PRIORITY
}
enum class FeePrecision(val symbol: String) {
PRECISE(""), CAN_BE_LOWER("<")
}
data class FeeState(
val selectedFeeType: FeeType = FeeType.NORMAL,
val feeList: List<Amount>? = null,
@ -21,6 +25,7 @@ data class FeeState(
val feeChipGroupIsVisible: Boolean = true,
val includeFeeSwitcherIsEnabled: Boolean = true,
val error: FeeAction.Error? = null,
val feePrecision: FeePrecision = FeePrecision.PRECISE
) : SendScreenState {
override val stateId: StateId = StateId.FEE

View file

@ -5,8 +5,8 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
@ -123,18 +123,18 @@ enum class ButtonState {
}
data class AmountState(
val amountToExtract: Amount? = null,
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: InputViewValue = InputViewValue(BigDecimal.ZERO.toPlainString()),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
val cursorAtTheSamePosition: Boolean = true,
val maxLengthOfAmount: Int = 2,
val decimalSeparator: String = ".",
val error: TapError? = null,
val inputIsEnabled: Boolean = true,
val amountToExtract: Amount? = null,
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: InputViewValue = InputViewValue(BigDecimal.ZERO.toPlainString()),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, FiatCurrency.Default.code),
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
val cursorAtTheSamePosition: Boolean = true,
val maxLengthOfAmount: Int = 2,
val decimalSeparator: String = ".",
val error: TapError? = null,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
override val stateId: StateId = StateId.AMOUNT
@ -146,7 +146,7 @@ data class AmountState(
fun createMainCurrency(type: MainCurrencyType, canSwitched: Boolean): MainCurrency {
return if (!canSwitched) MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE", false)
else when (type) {
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency)
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency.code)
MainCurrencyType.CRYPTO -> MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE")
}
}

View file

@ -14,13 +14,13 @@ import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
@ -36,7 +36,6 @@ import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
@ -244,7 +243,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
warningsAdapter = WarningMessagesAdapter()
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt()))
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
store.dispatch(SendAction.Warnings.Update)
@ -261,11 +260,10 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun restoreMainCurrency(): MainCurrencyType {
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
val mainCurrency = sp.getString("mainCurrency", TapCurrency.DEFAULT_FIAT_CURRENCY)
val foundType = MainCurrencyType.values()
.firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) }
?: MainCurrencyType.CRYPTO
return foundType
val mainCurrency = sp.getString("mainCurrency", FiatCurrency.Default.code)
return MainCurrencyType.values()
.firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) }
?: MainCurrencyType.CRYPTO
}
fun saveMainCurrency(type: MainCurrencyType) {

View file

@ -13,7 +13,9 @@ import androidx.compose.ui.platform.ComposeView
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen
import com.tangem.wallet.R
@ -48,16 +50,25 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment)
it.setTitle(R.string.add_custom_token_title)
}
val closePopupTrigger = initClosingPopupTriggerEvent()
view.findViewById<ComposeView>(R.id.view_compose)?.setContent {
AppCompatTheme(requireContext()) {
Box(modifier = Modifier
.fillMaxSize()
) {
AddCustomTokenScreen(state)
AddCustomTokenScreen(state, closePopupTrigger)
}
}
}
addBackPressHandler(this)
}
private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply {
onCloseComplete = ::handleOnBackPressed
addBackPressHandler(object : FragmentOnBackPressedHandler {
override fun handleOnBackPressed() = close()
})
}
}

View file

@ -21,21 +21,69 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.ToggledRippleTheme
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.compose.*
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestAddCustomTokenActions
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
private class AddCustomTokenScreen {} // for simple search
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
fun AddCustomTokenScreen(
state: MutableState<AddCustomTokenState>,
closePopupTrigger: ClosePopupTrigger,
) {
val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) }
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } }
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Surface(color = colorResource(id = R.color.lightGray5)) {
selectedTestCase.value.content(toggleBottomSheet)
}
},
sheetPeekHeight = 0.dp,
) {
Column() {
TestCasesList(onItemClick = {
selectedTestCase.value = it
toggleBottomSheet()
})
ScreenContent(state, closePopupTrigger)
}
}
ComposeDialogManager()
LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) })
DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } })
}
@OptIn(ExperimentalMaterialApi::class)
private suspend fun BottomSheetScaffoldState.toggle() {
if (bottomSheetState.isCollapsed) {
bottomSheetState.expand()
} else {
bottomSheetState.collapse()
}
}
@Composable
private fun ScreenContent(
state: MutableState<AddCustomTokenState>,
closePopupTrigger: ClosePopupTrigger,
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
@ -50,9 +98,8 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
) {
Box(Modifier.fillMaxSize()) {
LazyColumn(
contentPadding = PaddingValues(bottom = 90.dp)
contentPadding = PaddingValues(bottom = 90.dp),
) {
item { TestAddCustomTokenActions() }
item {
Surface(
modifier = Modifier.padding(16.dp),
@ -64,22 +111,21 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
.fillMaxWidth()
.padding(16.dp)
) {
FormFields(state)
FormFields(state, closePopupTrigger)
}
}
}
item { Warnings(state.value.warnings.toList()) }
}
}
ComposeDialogManager()
}
LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) })
DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } })
}
@Composable
private fun FormFields(state: MutableState<AddCustomTokenState>) {
private fun FormFields(
state: MutableState<AddCustomTokenState>,
closePopupTrigger: ClosePopupTrigger,
) {
val context = LocalContext.current
val errorConverter = remember { ModuleMessageConverter(context) }
@ -88,11 +134,11 @@ private fun FormFields(state: MutableState<AddCustomTokenState>) {
val data = ScreenFieldData.fromState(field, stateValue, errorConverter)
when (field.id) {
ContractAddress -> TokenContractAddressView(data)
Network -> TokenNetworkView(data, stateValue)
Network -> TokenNetworkView(data, stateValue, closePopupTrigger)
Name -> TokenNameView(data)
Symbol -> TokenSymbolView(data)
Decimals -> TokenDecimalsView(data)
DerivationPath -> TokenDerivationPathView(data, stateValue)
DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger)
}
}
}

View file

@ -86,7 +86,11 @@ fun TokenNameView(screenFieldData: ScreenFieldData) {
}
@Composable
fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) {
fun TokenNetworkView(
screenFieldData: ScreenFieldData,
state: AddCustomTokenState,
closePopupTrigger: ClosePopupTrigger,
) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
@ -98,6 +102,7 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
closePopupTrigger = closePopupTrigger,
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) }
SpacerH8()
}
@ -138,7 +143,11 @@ fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
}
@Composable
fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) {
fun TokenDerivationPathView(
screenFieldData: ScreenFieldData,
state: AddCustomTokenState,
closePopupTrigger: ClosePopupTrigger,
) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
@ -154,7 +163,8 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
TitleSubtitle(derivationPathName, blockchainName)
}
},
closePopupTrigger = closePopupTrigger,
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) }
SpacerH8()
}

View file

@ -0,0 +1,123 @@
package com.tangem.tap.features.tokens.addCustomToken.compose.test
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
/**
[REDACTED_AUTHOR]
*/
@Composable
fun ContractAddressTests(
onItemClick: VoidCallback
) {
val casesInfo = listOf(
"USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53",
"ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15",
"USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1",
"Custom EVM" to "0x1111111111111111112111111111111111111113",
"Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975",
"Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA",
)
CasesListContent(casesInfo, onItemClick)
}
@Composable
fun SolanaAddressTests(
onItemClick: VoidCallback
) {
val casesInfo = listOf(
"USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
"USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben",
"USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ",
"ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
)
CasesListContent(casesInfo, onItemClick)
}
@Composable
private fun CasesListContent(
casesList: List<Pair<String, String>>,
onItemClick: VoidCallback,
) {
LazyColumn(content = {
item {
Row() {
ResetContractAddressButton(onItemClick)
Text("", modifier = Modifier.weight(1f))
ResetAllFieldsButton(onItemClick)
}
Divider()
}
items(casesList.size) {
val (info, address) = casesList[it]
ContractAddressButton(info, address, onItemClick)
}
})
}
@Composable
fun ResetAllFieldsButton(
onItemClick: VoidCallback
) {
ActionButton(name = "Reset") {
onItemClick()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(Blockchain.Unknown, false)))
}
}
@Composable
fun ResetContractAddressButton(
onItemClick: VoidCallback
) {
ActionButton(name = "Set empty address") {
onItemClick()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
}
}
@Composable
private fun ContractAddressButton(
name: String,
address: String,
onItemClick: VoidCallback
) {
ActionButton(
modifier = Modifier.fillMaxWidth(),
name = name,
) {
onItemClick()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false)))
}
}
@Composable
fun ActionButton(
modifier: Modifier = Modifier,
name: String,
onClick: () -> Unit,
) {
Button(
modifier = modifier.padding(horizontal = 8.dp),
onClick = onClick
) { Text(name, fontSize = 12.sp) }
}

View file

@ -1,225 +0,0 @@
package com.tangem.tap.features.tokens.addCustomToken.compose.test
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
import com.tangem.domain.redux.domainStore
import com.tangem.wallet.BuildConfig
/**
[REDACTED_AUTHOR]
*/
@Composable
fun TestAddCustomTokenActions() {
if (!BuildConfig.TEST_ACTION_ENABLED) return
Column() {
// deep test
// ActionRow("Invalid field value") { InvalidFieldValue() }
// ActionRow("Active(true)") { ActiveTrue() }
// ActionRow("Active(false) && decimalCount != null") { ActiveFalseDecimals() }
// ActionRow("In several networks") { InSeveralNetworks() }
// ActionRow("Address not found") { UnknownContracts() }
// test
ActionRow("All in one") { AllInOne() }
// Any action
// ActionRow("CustomActions - find coins active=false, decimals != null") { CustomActions() }
}
}
@Composable
private fun AllInOne() {
// validation error
// ContractAddressButton(
// name = "invalid",
// address = "unk"
// )
// active = true
ContractAddressButton(
name = "true",
address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
)
// more than one network
ContractAddressButton(
name = ">1 network",
address = "0xa1faa113cbe53436df28ff0aee54275c13b40975"
)
// unknown
ContractAddressButton(
name = "unknown",
address = "0x1111111111111111112111111111111111111113"
)
ContractAddressButton(
name = "full custom",
address = "0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1",
tokenNetwork = Blockchain.Ethereum,
tokenName = "Test unknown",
tokenSymbol = "TU",
tokenDecimals = "5",
)
}
@Composable
private fun InvalidFieldValue() {
ContractAddressButton(
name = "unk",
address = "unk"
)
ContractAddressButton(
name = "someText",
address = "someText"
)
ContractAddressButton(
name = "alskdml...fa s",
address = "alskdmlasd asdln alsdknflasd flasd fa s"
)
}
@Composable
private fun ActiveTrue() {
ContractAddressButton(
name = "USDC- Ethereum",
address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
)
ContractAddressButton(
name = "USDT- Avalanche",
address = "0xc7198437980c041c805a1edcba50c1ce5db95118"
)
ContractAddressButton(
name = "VID- Fantom",
address = "0x922d641a426dcffaef11680e5358f34d97d112e1"
)
}
@Composable
private fun ActiveFalseDecimals() {
ContractAddressButton(
name = "ALPHA- avalanche",
address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f"
)
ContractAddressButton(
name = "BETA- avalanche",
address = "0x511d35c52a3c244e7b8bd92c0c297755fbd89212"
)
}
@Composable
private fun InSeveralNetworks() {
ContractAddressButton(
name = "ALPHA- Eth,Bsc",
address = "0xa1faa113cbe53436df28ff0aee54275c13b40975"
)
ContractAddressButton(
name = "BETA- Eth,Bsc",
address = "0xbe1a001fe942f96eea22ba08783140b9dcc09d28"
)
}
@Composable
private fun UnknownContracts() {
ContractAddressButton(
name = "0x11...113",
address = "0x1111111111111111112111111111111111111113"
)
ContractAddressButton(
name = "0xc7...111",
address = "0xc7198437980c041c805a1edcba50c1ce5db95111"
)
}
@Composable
private fun CustomActions() {
}
@Composable
private fun ActionRow(
name: String,
content: @Composable () -> Unit
) {
Column() {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = name,
fontSize = 14.sp
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) { content() }
}
}
@Composable
private fun ActionButton(
name: String,
onClick: VoidCallback,
) {
Button(
modifier = Modifier.padding(horizontal = 4.dp),
onClick = onClick
) { Text(name, fontSize = 8.sp) }
}
@Composable
private fun ContractAddressButton(
name: String,
address: String,
tokenNetwork: Blockchain? = null,
tokenName: String? = null,
tokenSymbol: String? = null,
tokenDecimals: String? = null,
) {
ActionButton(name = name) {
resetTokenValues()
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(address, false)))
tokenNetwork?.let {
domainStore.dispatch(OnTokenNetworkChanged(Field.Data(tokenNetwork, false)))
}
tokenName?.let {
domainStore.dispatch(OnTokenNameChanged(Field.Data(tokenName, false)))
}
tokenSymbol?.let {
domainStore.dispatch(OnTokenSymbolChanged(Field.Data(tokenSymbol, false)))
}
tokenDecimals?.let {
domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(tokenDecimals, false)))
}
}
}
@Composable
private fun CustomActionButton(
name: String,
action: suspend () -> Unit
) {
val startValue = 0
val anyValue = remember { mutableStateOf(startValue) }
LaunchedEffect(key1 = anyValue.value, block = {
if (anyValue.value != startValue) {
action()
}
})
ActionButton(name) { anyValue.value = anyValue.value + 1 }
}
private fun resetTokenValues() {
domainStore.dispatch(OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
// domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false)))
// domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false)))
// domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false)))
}

View file

@ -0,0 +1,63 @@
package com.tangem.tap.features.tokens.addCustomToken.compose.test
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import com.tangem.common.extensions.VoidCallback
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun TestCasesList(
onItemClick: (TestCase) -> Unit
) {
if (!BuildConfig.TEST_ACTION_ENABLED) return
Surface(
color = colorResource(id = R.color.lightGray5)
) {
Column(
Modifier.padding(horizontal = 16.dp)
) {
listOf(
TestCase.ContractAddress,
TestCase.SolanaTokens,
).map { TestCaseListItem(it, onItemClick) }
}
}
}
@Composable
fun TestCaseListItem(
testCase: TestCase,
onItemClick: (TestCase) -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = testCase.description,
)
Button(
onClick = { onItemClick(testCase) }
) { Text("Start") }
}
}
enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) {
ContractAddress("Test contract address field", { ContractAddressTests(it) }),
Auto("Test contract address field", { ContractAddressTests(it) }),
SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }), ;
}

View file

@ -17,7 +17,8 @@ sealed class TokensAction : Action {
val supportedBlockchains: List<Blockchain>? = null,
val scanResponse: ScanResponse? = null
) : TokensAction() {
data class Success(val currencies: List<Currency>) : TokensAction()
data class Success(val currencies: List<Currency>, val loadMore: Boolean) : TokensAction()
object Failure : TokensAction()
}
data class SetAddedCurrencies(
@ -32,4 +33,6 @@ sealed class TokensAction : Action {
) : TokensAction()
object PrepareAndNavigateToAddCustomToken : TokensAction()
data class SetSearchInput(val searchInput: String) : TokensAction()
}

View file

@ -8,11 +8,13 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.services.Result
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
@ -26,10 +28,10 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -40,27 +42,65 @@ class TokensMiddleware {
{ next ->
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action)
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse)
is TokensAction.SaveChanges -> handleSaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken(
action
)
is TokensAction.SetSearchInput -> {
handleLoadCurrencies(
scanResponse = store.state.globalState.scanResponse,
action.searchInput
)
}
}
next(action)
}
}
}
private fun handleLoadCurrencies(action: TokensAction.LoadCurrencies) {
val scanResponse = store.state.globalState.scanResponse
private fun handleLoadCurrencies(scanResponse: ScanResponse?, newSearchInput: String? = null) {
val tokensState = store.state.tokensState
val isTestcard = scanResponse?.card?.isTestCard ?: false
val supportedBlockchains: List<Blockchain> =
scanResponse?.card?.supportedBlockchains() ?: Blockchain.values().toList()
.filter { !it.isTestnet() }
val loadCoinsService = LoadAvailableCoinsService(
store.state.domainNetworks.tangemTechService,
currenciesRepository
)
scope.launch {
val currencies = async {
currenciesRepository.getSupportedTokens(isTestcard)
.filter(action.supportedBlockchains?.toSet())
val loadCoinsResult = if (newSearchInput == null) {
loadCoinsService.getSupportedTokens(
isTestcard,
supportedBlockchains,
tokensState.pageToLoad,
tokensState.searchInput
)
} else {
loadCoinsService.getSupportedTokens(
isTestcard, supportedBlockchains, 0, newSearchInput.ifBlank { null }
)
}
val delay = async { delay(600) }
delay.await()
store.dispatchOnMain(TokensAction.LoadCurrencies.Success(currencies.await()))
when (loadCoinsResult) {
is Result.Success -> {
val currencies = loadCoinsResult.data.currencies
.filter(supportedBlockchains.toSet())
store.dispatchOnMain(
TokensAction.LoadCurrencies.Success(
currencies, loadCoinsResult.data.moreAvailable
)
)
}
is Result.Failure -> store.dispatchOnMain(TokensAction.LoadCurrencies.Failure)
}
}
}
@ -75,19 +115,22 @@ class TokensMiddleware {
)
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
val blockchainsToRemove = currentBlockchains.filter { !action.addedBlockchains.contains(it) }
val blockchainsToRemove =
currentBlockchains.filter { !action.addedBlockchains.contains(it) }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
val tokensToRemove = currentTokens.filter {
token -> !action.addedTokens.any { it.token == token.token }
val tokensToRemove = currentTokens.filter { token ->
!action.addedTokens.any { it.token == token.token }
}
val derivationStyle = scanResponse.card.derivationStyle
removeCurrenciesIfNeeded(convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
derivationStyle = derivationStyle
))
removeCurrenciesIfNeeded(
convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
derivationStyle = derivationStyle
)
)
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
store.dispatchDebugErrorNotification("Nothing to save")
@ -247,7 +290,8 @@ class TokensMiddleware {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork = BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
val blockchainNetwork =
BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork)
}
}
@ -298,8 +342,15 @@ class TokensMiddleware {
walletStore.walletsData.map { walletData -> walletData.currency }
}.flatten().map {
when (it) {
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(it.blockchain, it.derivationPath)
is Currency.Token -> DomainWrapped.Currency.Token(it.token, it.blockchain, it.derivationPath)
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(
it.blockchain,
it.derivationPath
)
is Currency.Token -> DomainWrapped.Currency.Token(
it.token,
it.blockchain,
it.derivationPath
)
}
}
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))

View file

@ -15,9 +15,24 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
val tokensState = state.tokensState
return when (action) {
is TokensAction.ResetState -> TokensState()
is TokensAction.LoadCurrencies -> tokensState.copy(scanResponse = action.scanResponse)
is TokensAction.LoadCurrencies -> {
val loadingState = if (tokensState.currencies.isEmpty()) {
LoadCoinsState.LOADING
} else {
LoadCoinsState.LOADED
}
tokensState.copy(
scanResponse = action.scanResponse,
loadCoinsState = loadingState
)
}
is TokensAction.LoadCurrencies.Success -> {
tokensState.copy(currencies = action.currencies)
tokensState.copy(
currencies = tokensState.currencies + action.currencies,
needToLoadMore = action.loadMore,
pageToLoad = tokensState.pageToLoad + 1,
loadCoinsState = LoadCoinsState.LOADED
)
}
is TokensAction.SetAddedCurrencies -> {
@ -38,6 +53,15 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
is TokensAction.AllowToAddTokens -> {
tokensState.copy(allowToAdd = action.allow)
}
is TokensAction.SetSearchInput -> {
tokensState.copy(
searchInput = action.searchInput.ifBlank { null },
needToLoadMore = true,
currencies = emptyList(),
pageToLoad = 0,
loadCoinsState = LoadCoinsState.LOADING
)
}
else -> tokensState
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.wallet.redux.WalletData
@ -16,10 +17,19 @@ data class TokensState(
val nonRemovableTokens: List<ContractAddress> = emptyList(),
val nonRemovableBlockchains: List<Blockchain> = emptyList(),
val currencies: List<Currency> = emptyList(),
val searchInput: String? = null,
val allowToAdd: Boolean = true,
val derivationStyle: DerivationStyle? = null,
val scanResponse: ScanResponse? = null
) : StateType
val scanResponse: ScanResponse? = null,
val needToLoadMore: Boolean = true,
val pageToLoad: Int = 0,
val loadCoinsState: LoadCoinsState = LoadCoinsState.LOADING,
) : StateType {
fun canHandleToken(token: TokenWithBlockchain): Boolean {
return scanResponse?.card?.canHandleToken(token.blockchain) ?: false
}
}
typealias ContractAddress = String
@ -62,10 +72,14 @@ fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency
it.copy(contracts =
it.contracts.filter {
supportedBlockchains.contains(it.blockchain) &&
(it.blockchain.canHandleTokens() || it.address == null)
(it.blockchain.canHandleTokens() || it.address == null)
}
)
}.filterNot {
it.contracts.isNullOrEmpty() && !supportedBlockchains.contains(Blockchain.fromNetworkId(it.id))
}
}
enum class LoadCoinsState {
LOADING, LOADED, ERROR
}

View file

@ -17,6 +17,7 @@ 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.dispatchOnMain
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.ContractAddress
@ -46,6 +47,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
})
val inflater = TransitionInflater.from(requireContext())
@ -81,13 +83,17 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
store.dispatchNotification(R.string.contract_address_copied_message)
}
val onLoadMore = {
store.dispatch(TokensAction.LoadCurrencies(scanResponse = store.state.globalState.scanResponse))
}
cvCurrencies.setContent {
AppCompatTheme {
CurrenciesScreen(
tokensState = tokensState,
searchInput = searchInput,
onSaveChanges = onSaveChanges,
onNetworkItemClicked = onNetworkItemClicked
onNetworkItemClicked = onNetworkItemClicked,
onLoadMore = onLoadMore
)
}
}
@ -123,10 +129,11 @@ 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)
.debounce(800)
.distinctUntilChanged()
.onEach {
searchInput.value = it.lowercase()
dispatchOnMain(TokensAction.SetSearchInput(searchInput.value))
}
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater)

View file

@ -22,9 +22,12 @@ import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.pixelsToDp
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.LoadCoinsState
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.store
@ -33,11 +36,11 @@ 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
onNetworkItemClicked: (ContractAddress) -> Unit,
onLoadMore: () -> Unit
) {
val context = LocalContext.current
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
@ -74,10 +77,10 @@ fun CurrenciesScreen(
) {
AnimatedVisibility(
visible = tokensState.value.currencies.isEmpty(),
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADING,
enter = fadeIn(),
exit = fadeOut()
){
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
@ -88,22 +91,35 @@ fun CurrenciesScreen(
}
}
AnimatedVisibility(
visible = tokensState.value.currencies.isNotEmpty(),
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADED,
enter = fadeIn(animationSpec = tween(1000)),
exit = fadeOut(animationSpec = tween(1000))
) {
Column {
if (tokensState.value.scanResponse?.card?.useOldStyleDerivation == true) CurrenciesWarning()
val showHeader = tokensState.value.scanResponse?.card?.useOldStyleDerivation == true
ListOfCurrencies(
header = { if (showHeader) CurrenciesWarning() },
currencies = tokensState.value.currencies,
nonRemovableTokens = tokensState.value.nonRemovableTokens,
nonRemovableBlockchains = tokensState.value.nonRemovableBlockchains,
addedTokens = addedTokensState.value,
addedBlockchains = addedBlockchainsState.value,
searchInput = searchInput.value,
allowToAdd = tokensState.value.allowToAdd,
onAddCurrencyToggled = onAddCurrencyToggleClick,
onNetworkItemClicked = onNetworkItemClicked
onAddCurrencyToggled = { currency, token ->
onAddCurrencyToggleClick(currency, token)
token?.let {
if (!tokensState.value.canHandleToken(it)) {
val dialog = AppDialog.SimpleOkDialog(
header = context.getString(R.string.common_warning),
message = context.getString(R.string.alert_manage_tokens_unsupported_message)
) { onAddCurrencyToggleClick(currency, it) }
store.dispatchDialogShow(dialog)
}
}
},
onNetworkItemClicked = onNetworkItemClicked,
onLoadMore = onLoadMore
)
}
}

View file

@ -3,13 +3,15 @@ package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.compose.extensions.OnBottomReached
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@ -17,15 +19,16 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@Composable
fun ListOfCurrencies(
header: @Composable () -> Unit,
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
onNetworkItemClicked: (ContractAddress) -> Unit,
onLoadMore: () -> Unit
) {
val expandedCurrencies = remember { mutableStateOf(listOf("")) }
@ -40,21 +43,16 @@ fun ListOfCurrencies(
expandedCurrencies.value = mutableList
}
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(bottom = 90.dp)
) {
val filteredCurrencies = if (searchInput.isBlank()) {
currencies
} else {
currencies.asSequence().filter {
it.name.lowercase().contains(searchInput) || it.symbol.lowercase()
.contains(searchInput)
}.toList()
}
items(filteredCurrencies) { currency ->
item { header() }
itemsIndexed(currencies) { index, currency ->
CurrencyItem(
currency = currency,
nonRemovableTokens = nonRemovableTokens,
@ -68,8 +66,8 @@ fun ListOfCurrencies(
onNetworkItemClicked = onNetworkItemClicked
)
}
}
listState.OnBottomReached(loadMoreThreshold = 40) { onLoadMore() }
}
val Currency.fullName: String

View file

@ -1,20 +1,26 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.extensions.toSendableAmounts
import java.math.BigDecimal
data class PendingTransaction(
val address: String?,
val amount: BigDecimal?,
val amountUi: String?,
val currency: String,
val type: PendingTransactionType
)
val transactionData: TransactionData,
val type: PendingTransactionType,
) {
val address: String? = when (type) {
PendingTransactionType.Incoming -> transactionData.destinationAddress
PendingTransactionType.Outgoing -> transactionData.sourceAddress
PendingTransactionType.Unknown -> null
}
val amountValue: BigDecimal? = transactionData.amount.value
val amountValueUi: String? = amountValue?.toFormattedString(transactionData.amount.decimals)
val currency: String = transactionData.amount.currencySymbol
}
enum class PendingTransactionType { Incoming, Outgoing, Unknown }
@ -22,29 +28,11 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact
if (this.status == TransactionStatus.Confirmed) return null
val type: PendingTransactionType = when {
this.sourceAddress == walletAddress -> {
PendingTransactionType.Outgoing
}
this.destinationAddress == walletAddress -> {
PendingTransactionType.Incoming
}
else -> {
PendingTransactionType.Unknown
}
this.sourceAddress == walletAddress -> PendingTransactionType.Outgoing
this.destinationAddress == walletAddress -> PendingTransactionType.Incoming
else -> PendingTransactionType.Unknown
}
val address = if (this.sourceAddress == walletAddress) {
this.destinationAddress
} else {
this.sourceAddress
}
return PendingTransaction(
if (address == "unknown") null else address,
this.amount.value,
this.amount.value?.toFormattedString(amount.decimals),
this.amount.currencySymbol,
type
)
return PendingTransaction(this, type)
}
fun List<TransactionData>.toPendingTransactions(walletAddress: String): List<PendingTransaction> {
@ -55,27 +43,58 @@ fun List<PendingTransaction>.removeUnknownTransactions(): List<PendingTransactio
return this.filter { it.type != PendingTransactionType.Unknown }
}
fun List<PendingTransaction>.filterByCoin(): List<PendingTransaction> {
return this.filter { it.transactionData.amount.type == AmountType.Coin }
}
fun List<PendingTransaction>.filterByToken(token: Token): List<PendingTransaction> {
return this.filter { it.currency == token.symbol }
}
fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: String): PendingTransaction? {
if (this.amount.currencySymbol != token.symbol) return null
return this.toPendingTransaction(walletAddress)
}
fun List<TransactionData>.toPendingTransactionsForToken(token: Token, walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}
fun Wallet.getPendingTransactions(type: PendingTransactionType? = null): List<PendingTransaction> {
val txs = recentTransactions.toPendingTransactions(address)
return when(type) {
return when (type) {
null -> txs
else -> txs.filter { it.type == type }
}
}
fun Wallet.getPendingTransactions(token: Token): List<PendingTransaction> {
return recentTransactions.mapNotNull { it.toPendingTransactionForToken(token, address) }
}
fun Wallet.hasPendingTransactions(): Boolean {
return getPendingTransactions().isNotEmpty()
}
fun Wallet.hasPendingTransactions(token: Token): Boolean {
return getPendingTransactions(token).isNotEmpty()
}
fun Wallet.getSendableAmounts(): List<Amount> {
return amounts.values
.filter { it.type != AmountType.Reserve }
.filter { it.isAboveZero() }
}
fun Wallet.hasSendableAmounts(): Boolean {
return getSendableAmounts().isNotEmpty()
}
fun Wallet.hasSendableAmountsOrPendingTransactions(): Boolean {
return hasPendingTransactions() || amounts.toSendableAmounts().isNotEmpty()
return hasPendingTransactions() || hasSendableAmounts()
}
fun Wallet.isSendableAmount(type: AmountType): Boolean {
return amounts[type]?.isAboveZero() == true
}
fun Wallet.isSendableAmount(token: Token): Boolean {
return isSendableAmount(AmountType.Token(token))
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.features.wallet.models
import com.tangem.tap.common.entities.FiatCurrency
import java.math.BigDecimal
data class TotalBalance(
val state: State,
val fiatAmount: BigDecimal,
val fiatCurrency: FiatCurrency,
) {
enum class State {
Loading,
SomeTokensFailed,
Success,
}
}

View file

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

View file

@ -4,11 +4,12 @@ import android.content.Context
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.card.Card
import com.tangem.tap.common.entities.FiatCurrency
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.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -98,7 +99,10 @@ sealed class WalletAction : Action {
data class LoadFiatRate(
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
) : WalletAction() {
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
data class Success(
val fiatRates: Map<Currency, BigDecimal?>
) : WalletAction()
object Failure : WalletAction()
}
@ -113,10 +117,8 @@ sealed class WalletAction : Action {
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
object EmptyField : WalletAction(), ErrorAction {
override val error = TapError.PayIdEmptyField
}
@ -129,13 +131,18 @@ sealed class WalletAction : Action {
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object SignedHashesMultiWalletDialog : WalletAction()
object ChooseTradeActionDialog : WalletAction()
}
sealed class DialogAction : WalletAction() {
data class QrCode(
val currency: Currency,
val selectedAddress: AddressData,
) : DialogAction()
object HideDialog : WalletAction()
object SignedHashesMultiWalletDialog : DialogAction()
object ChooseTradeActionDialog : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
object Hide : DialogAction()
}
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
@ -157,10 +164,15 @@ sealed class WalletAction : Action {
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class SetWalletRent(
val blockchain: BlockchainNetwork,
val wallet: Wallet,
val minRent: String,
val rentExempt: String
) : WalletAction()
data class RemoveWalletRent(val blockchain: BlockchainNetwork) : WalletAction()
data class RemoveWalletRent(val wallet: Wallet) : WalletAction()
sealed class AppCurrencyAction : WalletAction() {
object ChooseAppCurrency : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
}
}

View file

@ -3,11 +3,12 @@ package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.CryptoCurrencyName
@ -15,13 +16,12 @@ import com.tangem.tap.common.toggleWidget.WidgetState
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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findTotalBalanceState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -35,7 +35,6 @@ data class WalletState(
val error: ErrorType? = null,
val cardImage: Artwork? = null,
val hashesCountVerified: Boolean? = null,
val walletDialog: StateDialog? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val wallets: List<WalletStore> = listOf(),
val isMultiwalletAllowed: Boolean = false,
@ -44,6 +43,7 @@ data class WalletState(
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val totalBalance: TotalBalance? = null,
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -140,16 +140,13 @@ data class WalletState(
val wallet = walletManager.wallet
if (walletData.currency is Currency.Blockchain) {
return wallet.recentTransactions.toPendingTransactions(wallet.address).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 when (walletData.currency) {
is Currency.Blockchain -> !wallet.hasPendingTransactions() && !wallet.hasSendableAmounts()
is Currency.Token -> {
val token = walletData.currency.token
!wallet.hasPendingTransactions(token) && !wallet.isSendableAmount(token)
}
}
}
return false
}
@ -189,25 +186,28 @@ data class WalletState(
.distinct().map { getWalletStore(it) }.mapNotNull { it?.updateWallets(walletsData) }
return updateWalletStores(walletStores)
}
fun updateWalletStore(walletStore: WalletStore?): WalletState {
return copy(wallets = replaceWalletInWallets(walletStore))
.updateTotalBalance()
}
fun updateWalletStores(walletStores: List<WalletStore>): WalletState {
val walletStores = walletStores.toMutableList()
private fun updateWalletStores(walletStores: List<WalletStore>): WalletState {
val walletStoresMutable = walletStores.toMutableList()
val updatedWallets = wallets.map { oldWalletStore ->
val walletStore = walletStores.find { it.blockchainNetwork == oldWalletStore.blockchainNetwork }
val walletStore = walletStoresMutable.find {
it.blockchainNetwork == oldWalletStore.blockchainNetwork
}
if (walletStore != null) {
walletStores.remove(walletStore)
walletStoresMutable.remove(walletStore)
walletStore
} else {
oldWalletStore
}
}
return copy(wallets = updatedWallets + walletStores)
return copy(wallets = updatedWallets + walletStoresMutable)
.updateTotalBalance()
}
fun removeWallet(walletData: WalletData?): WalletState {
@ -268,12 +268,31 @@ data class WalletState(
)
}
}
private fun updateTotalBalance(): WalletState {
return if (wallets.isNotEmpty()) {
val walletsData = wallets.flatMap(WalletStore::walletsData)
this.copy(
totalBalance = TotalBalance(
state = walletsData.findTotalBalanceState(),
fiatAmount = walletsData.calculateTotalFiatAmount(),
fiatCurrency = store.state.globalState.appCurrency
)
)
} else this.copy(
totalBalance = null
)
}
}
sealed class WalletDialog : StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
object SignedHashesMultiWalletDialog : WalletDialog()
object ChooseTradeActionDialog : WalletDialog()
sealed interface WalletDialog : StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog
object SignedHashesMultiWalletDialog : WalletDialog
object ChooseTradeActionDialog : WalletDialog
data class CurrencySelectionDialog(
val currenciesList: List<FiatCurrency>,
val currentAppCurrency: FiatCurrency,
) : WalletDialog
}
enum class ProgressState : WidgetState { Loading, Done, Error }
@ -345,31 +364,49 @@ data class WalletData(
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency,
val warningRent: WalletRent? = null,
val walletRent: WalletRent? = null,
) {
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false
return listOfAddresses.size > 1
}
fun shouldShowCoinAmountWarning(): Boolean = when (currency) {
is Currency.Blockchain -> false
is Currency.Token -> blockchainAmountIsEmpty() && !tokenAmountIsEmpty()
fun shouldEnableTokenSendButton(): Boolean = if (blockchainAmountIsEmpty()) {
false
} else {
!tokenAmountIsEmpty()
}
fun shouldEnableTokenSendButton(): Boolean = !blockchainAmountIsEmpty() || !tokenAmountIsEmpty()
fun assembleWarnings(): List<WalletWarning> {
val blockchain = currency.blockchain
val walletWarnings = mutableListOf<WalletWarning>()
if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress)
}
if (currency.isBlockchain()) {
if (blockchain == Blockchain.Solana || blockchain == Blockchain.SolanaTestnet) {
val card = store.state.globalState.scanResponse?.card
if (card?.canHandleToken(blockchain) == false) {
walletWarnings.add(WalletWarning.SolanaTokensUnsupported)
}
}
}
if (walletRent != null) {
walletWarnings.add(WalletWarning.Rent(walletRent))
}
if (!currency.isBlockchain() && (blockchainAmountIsEmpty() && !tokenAmountIsEmpty())) {
val fullName = currency.blockchain.fullName
walletWarnings.add(WalletWarning.BalanceNotEnoughForFee(fullName))
}
private fun blockchainAmountIsEmpty(): Boolean =
currencyData.blockchainAmount?.isZero() ?: false
return walletWarnings.sortedBy { it.showingPosition }
}
private fun blockchainAmountIsEmpty(): Boolean = currencyData.blockchainAmount?.isZero() == true
private fun tokenAmountIsEmpty(): Boolean = currencyData.amount?.isZero() == true
}
data class WalletRent(
val minRentValue: String,
val rentExemptValue: String
)
sealed interface Currency {
val coinId: String?

View file

@ -0,0 +1,78 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.persistence.FiatCurrenciesPrefStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
class AppCurrencyMiddleware(
private val tangemTechService: TangemTechService,
private val tapWalletManager: TapWalletManager,
private val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage,
private val appCurrencyProvider: () -> FiatCurrency,
) {
fun handle(action: WalletAction.AppCurrencyAction) {
when (action) {
is WalletAction.AppCurrencyAction.ChooseAppCurrency -> showSelector()
is WalletAction.AppCurrencyAction.SelectAppCurrency -> selectCurrency(action)
}
}
private fun showSelector() {
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatchDialogShow(
WalletDialog.CurrencySelectionDialog(
currenciesList = storedFiatCurrencies.mapToUiModel(),
currentAppCurrency = appCurrencyProvider.invoke()
)
)
}
scope.launch {
when (val result = tangemTechService.currencies()) {
is Result.Success -> {
val currenciesList = result.data.currencies
if (currenciesList.isNotEmpty() &&
currenciesList.toSet() != storedFiatCurrencies.toSet()
) {
fiatCurrenciesPrefStorage.save(currenciesList)
store.dispatchDialogShow(
WalletDialog.CurrencySelectionDialog(
currenciesList = currenciesList.mapToUiModel(),
currentAppCurrency = appCurrencyProvider.invoke()
)
)
}
}
is Result.Failure -> {}
}
}
}
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
tapWalletManager.rates.clear()
fiatCurrenciesPrefStorage.saveAppCurrency(action.fiatCurrency)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletAction.LoadFiatRate())
}
private fun List<CurrenciesResponse.Currency>.mapToUiModel(): List<FiatCurrency> {
return this.map {
FiatCurrency(
code = it.code,
name = it.name,
symbol = it.unit
)
}
}
}

View file

@ -9,7 +9,7 @@ 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.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.Currency

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
@ -51,7 +52,10 @@ class TradeCryptoMiddleware {
currency is Currency.Token && currency.blockchain.isTestnet()
) {
val walletManager = store.state.walletState.getWalletManager(currency)
if (walletManager !is EthereumWalletManager) return
if (walletManager !is EthereumWalletManager) {
store.dispatchDebugErrorNotification("Testnet tokens available only for the ETH")
return
}
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }
return
@ -61,8 +65,9 @@ class TradeCryptoMiddleware {
action = exchangeAction,
blockchain = currency.blockchain,
cryptoCurrencyName = currencySymbol,
fiatCurrency = appCurrency,
walletAddress = defaultAddress)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
fiatCurrencyName = appCurrency.code,
walletAddress = defaultAddress
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.store
class WalletDialogsMiddleware {
fun handle(action: WalletAction.DialogAction) {
when (action) {
is WalletAction.DialogAction.SignedHashesMultiWalletDialog -> {
store.dispatchDialogShow(WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.DialogAction.ChooseTradeActionDialog -> {
store.dispatchDialogShow(WalletDialog.ChooseTradeActionDialog)
}
is WalletAction.DialogAction.QrCode -> {
store.dispatchDialogShow(
AppDialog.AddressInfoDialog(
currency = action.currency,
addressData = action.selectedAddress,
)
)
}
is WalletAction.DialogAction.ChooseCurrency -> {
store.dispatchDialogShow(
WalletDialog.SelectAmountToSendDialog(
amounts = action.amounts
)
)
}
is WalletAction.DialogAction.Hide -> {
store.dispatchDialogHide()
}
}
}
}

View file

@ -16,22 +16,21 @@ import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.getSendableAmounts
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -48,6 +47,15 @@ class WalletMiddleware {
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
private val warningsMiddleware = WarningsMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
private val walletDialogMiddleware = WalletDialogsMiddleware()
private val appCurrencyMiddleware by lazy(mode = LazyThreadSafetyMode.NONE) {
AppCurrencyMiddleware(
tangemTechService = store.state.domainNetworks.tangemTechService,
tapWalletManager = store.state.globalState.tapWalletManager,
fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
@ -72,6 +80,8 @@ class WalletMiddleware {
walletState,
globalState
)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is WalletAction.DialogAction -> walletDialogMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
@ -104,7 +114,7 @@ class WalletMiddleware {
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.LoadFiatRate -> {
val appCurrencyId = globalState.appCurrency
val appCurrencyId = globalState.appCurrency.code
scope.launch {
val coinsList = when {
action.wallet != null -> {
@ -122,11 +132,15 @@ class WalletMiddleware {
)
when (ratesResult) {
is Result.Success -> {
ratesResult.data.loadedRates.forEach {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it.toPair()))
ratesResult.data.loadedRates.let {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it))
}
ratesResult.data.failedRates.forEach { (currency, throwable) ->
Timber.e(throwable, "Loading rates failed for [%s]", currency.currencySymbol)
Timber.e(
throwable,
"Loading rates failed for [%s]",
currency.currencySymbol
)
}
}
is Result.Failure -> {
@ -219,15 +233,6 @@ class WalletMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData =
walletState.getWalletData(walletState.selectedCurrency) ?: return
val selectedAddressData =
selectedWalletData.walletAddresses?.selectedAddress ?: return
val currency = selectedWalletData.currency
store.dispatchDialogShow(AppDialog.AddressInfoDialog(currency, selectedAddressData))
}
}
}
@ -243,13 +248,13 @@ class WalletMiddleware {
PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletStore?.walletManager)
}
} else {
val amounts = walletStore?.walletManager?.wallet?.amounts?.toSendableAmounts()
val amounts = walletStore?.walletManager?.wallet?.getSendableAmounts()
if (currency != null && state.isMultiwalletAllowed) {
when (currency) {
is Currency.Blockchain -> {
val amountToSend =
amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData.fiatRate,
@ -259,7 +264,7 @@ class WalletMiddleware {
is Currency.Token -> {
val amountToSend =
amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.Send.ChooseCurrency(amounts)
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
prepareSendActionForToken(
amount = amountToSend,
state = state,
@ -322,7 +327,7 @@ class WalletMiddleware {
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val outgoingAmount = outgoingTxs.sumOf { it.amountValue ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
@ -330,18 +335,16 @@ class WalletMiddleware {
val currency = walletManager.wallet.blockchain.currency
if (show) {
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
wallet = walletManager.wallet,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
))
dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet))
}
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
class AppCurrencyReducer {
fun reduce(
action: WalletAction.AppCurrencyAction,
state: WalletState,
): WalletState {
return when (action) {
is WalletAction.AppCurrencyAction.SelectAppCurrency,
is WalletAction.AppCurrencyAction.ChooseAppCurrency -> state
}
}
}

View file

@ -2,15 +2,22 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.common.extensions.guard
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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
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.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -106,17 +113,16 @@ class MultiWalletReducer {
throw NullPointerException("MultiWallet.TokenLoaded: WalletManager must be no NULL")
}
val pendingTransactions = wallet.recentTransactions.toPendingTransactions(wallet.address)
val sendButtonEnabled =
action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val tokenPendingTransactions = pendingTransactions
.filter { it.currency == action.amount.currencySymbol }
val pendingTransactions = wallet.getPendingTransactions()
val tokenPendingTransactions = pendingTransactions.filterByToken(action.token)
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(currency)
val isTokenSendButtonEnabled = action.amount.isAboveZero() && pendingTransactions.isEmpty()
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
@ -126,12 +132,12 @@ class MultiWalletReducer {
),
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
action.amount.value
?.toFiatString(it, store.state.globalState.appCurrency)
?.toFiatString(it, store.state.globalState.appCurrency.symbol)
},
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
currency = Currency.Token(
token = action.token,
blockchain = action.blockchain.blockchain,

View file

@ -8,10 +8,16 @@ 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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.TradeCryptoState
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -33,31 +39,27 @@ class OnWalletLoadedReducer {
blockchainNetwork: BlockchainNetwork,
walletState: WalletState
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState
val fiatCurrency = store.state.globalState.appCurrency
val exchangeManager = store.state.globalState.currencyExchangeManager
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
if (walletState.getWalletData(blockchainNetwork) == null) {
return walletState
}
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency
)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val coinSendButton = coinAmountValue?.isZero() == false && pendingTransactions.isEmpty()
val pendingTransactions = wallet.getPendingTransactions()
val isCoinSendButtonEnabled = coinAmountValue?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.getWalletData(blockchainNetwork)
val fiatAmount = walletData?.fiatRate?.let { coinAmountValue?.toFiatValue(it) }
val newWalletData = walletData?.copy(
val fiatAmount = walletData.fiatRate?.let { coinAmountValue?.toFiatValue(it) }
val newWalletData = walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus, currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
@ -65,10 +67,10 @@ class OnWalletLoadedReducer {
amount = coinAmountValue,
amountFormatted = formattedAmount,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencySymbol)
fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(coinSendButton),
mainButton = WalletMainButton.SendButton(isCoinSendButtonEnabled),
currency = Currency.fromBlockchainNetwork(blockchainNetwork),
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData),
)
@ -76,8 +78,7 @@ class OnWalletLoadedReducer {
val tokens = wallet.getTokens().mapNotNull { token ->
val currency = Currency.fromBlockchainNetwork(blockchainNetwork, token)
val tokenWalletData = walletState.getWalletData(currency)
val tokenPendingTransactions =
pendingTransactions.filter { it.currency == token.symbol }
val tokenPendingTransactions = pendingTransactions.filterByToken(token)
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
@ -87,8 +88,8 @@ class OnWalletLoadedReducer {
val tokenFiatAmount =
tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate) }
val tokenSendButton = newWalletData?.shouldEnableTokenSendButton() == true
&& tokenPendingTransactions.isEmpty()
val isTokenSendButtonEnabled = newWalletData.shouldEnableTokenSendButton()
&& tokenPendingTransactions.isEmpty()
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
@ -99,14 +100,14 @@ class OnWalletLoadedReducer {
token.symbol
),
fiatAmount = tokenFiatAmount,
fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrencySymbol)
fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
),
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(tokenSendButton),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData),
)
}
val newWallets = (tokens + newWalletData).mapNotNull { it }
val newWallets = tokens + newWalletData
val wallets = walletState.replaceSomeWallets((newWallets))
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
@ -114,16 +115,18 @@ class OnWalletLoadedReducer {
} else {
ProgressState.Done
}
val newState = walletState.updateWalletsData(wallets)
return newState.copy(
state = state, error = null
)
return walletState
.updateWalletsData(wallets)
.copy(
state = state,
error = null
)
}
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
val fiatCurrencySymbol = store.state.globalState.appCurrency
val fiatCurrencyName = store.state.globalState.appCurrency.code
val exchangeManager = store.state.globalState.currencyExchangeManager
val token = wallet.getFirstToken()
@ -132,7 +135,7 @@ class OnWalletLoadedReducer {
if (tokenAmount != null) {
val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate
val tokenFiatAmount =
tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) }
tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencyName) }
TokenData(
tokenAmount.value?.toFormattedCurrencyString(
token.decimals, token.symbol
@ -152,9 +155,9 @@ class OnWalletLoadedReducer {
)
val fiatRate = walletState.primaryWallet?.fiatRate
val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencyName) }
val pendingTransactions = wallet.recentTransactions.toPendingTransactions(wallet.address)
val pendingTransactions = wallet.getPendingTransactions()
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress

View file

@ -0,0 +1,48 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import java.math.BigDecimal
fun List<WalletData>.findTotalBalanceState(): TotalBalance.State {
return this.mapToTotalBalanceState()
.reduce(TotalBalance.State::or)
}
fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
return this.map { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
fun List<WalletData>.mapToTotalBalanceState(): List<TotalBalance.State> {
return this.map {
when (it.currencyData.status) {
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress -> TotalBalance.State.Success
BalanceStatus.Unreachable,
BalanceStatus.NoAccount,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain -> TotalBalance.State.SomeTokensFailed
BalanceStatus.Loading,
null -> TotalBalance.State.Loading
}
}
}
infix fun TotalBalance.State.or(newState: TotalBalance.State): TotalBalance.State {
return when (this) {
TotalBalance.State.Loading -> when (newState) {
TotalBalance.State.Loading,
TotalBalance.State.SomeTokensFailed,
TotalBalance.State.Success -> this
}
TotalBalance.State.Success,
TotalBalance.State.SomeTokensFailed -> when (newState) {
TotalBalance.State.Loading,
TotalBalance.State.SomeTokensFailed -> newState
TotalBalance.State.Success -> this
}
}
}

View file

@ -3,24 +3,37 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatString
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.common.redux.AppState
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.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.TradeCryptoState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletAddresses
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
import java.math.RoundingMode
import org.rekotlin.Action
class WalletReducer {
companion object {
@ -32,6 +45,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val multiWalletReducer = MultiWalletReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
val appCurrencyReducer = AppCurrencyReducer()
if (action !is WalletAction) return state.walletState
@ -70,9 +84,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallets = newState.wallets
.map {
it.copy(
walletsData = it.walletsData.map {
.map { store ->
store.copy(
walletsData = store.walletsData.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable
@ -110,6 +124,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
else -> { /* no-op */
}
}
}
@ -146,12 +162,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
val currencies = listOf(Currency.fromBlockchainNetwork(action.blockchain)) +
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(
action.blockchain,
it
)
}
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(action.blockchain, it)
}
val newWallets = newState.walletsData.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
@ -261,7 +274,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
is WalletAction.LoadFiatRate.Success ->
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
newState = setNewFiatRate(action.fiatRates, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (state.twinCardsState.cardNumber) {
@ -271,21 +284,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowDialog.SignedHashesMultiWalletDialog -> {
newState = newState.copy(walletDialog = WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.ShowDialog.ChooseTradeActionDialog -> {
newState = newState.copy(walletDialog = WalletDialog.ChooseTradeActionDialog)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
is WalletAction.Send.ChooseCurrency -> {
newState = newState.copy(
walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts)
)
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.TradeCryptoAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val selectedWalletData = newState.getWalletData(newState.selectedCurrency)
@ -305,23 +303,20 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
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.updateWalletsData(listOf(walletData))
}
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletRent = WalletRent(action.minRent, action.rentExempt)
val walletsData = walletStore.walletsData.map { it.copy(walletRent = walletRent) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.RemoveWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData == null) {
newState
} else {
walletData = walletData.copy(warningRent = null)
newState = newState.updateWalletsData(listOf(walletData))
}
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletsData = walletStore.walletsData.map { it.copy(walletRent = null) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}
else -> { /* no-op */
}
}
return newState
@ -376,56 +371,83 @@ private fun handleCheckSignedHashesActions(
private fun setNewFiatRate(
fiatRate: Pair<Currency, BigDecimal?>,
appCurrency: FiatCurrencyName, state: WalletState
fiatRates: Map<Currency, BigDecimal?>,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val rate = fiatRate.second ?: return state
val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency, RoundingMode.HALF_UP)
val currency = fiatRate.first
val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal ->
rate.toFiatRateString(
fiatCurrencyName = appCurrency.symbol
)
}
return if (!state.isMultiwalletAllowed) {
setSingeWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
return if (state.isMultiwalletAllowed) {
setMultiWalletFiatRate(
fiatRates = fiatRates.mapNotNullValues { it.value },
rateFormatter = rateFormatter,
appCurrency = appCurrency,
state = state
)
} else {
setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
val fiatRate = fiatRates.entries.firstOrNull()
val currency = fiatRate?.key ?: return state
val rate = fiatRate.value ?: return state
setSingleWalletFiatRate(
rate = rate,
rateFormatted = rateFormatter(rate),
currency = currency,
appCurrency = appCurrency,
state = state
)
}
}
private fun setMultiWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: Currency,
appCurrency: FiatCurrencyName, state: WalletState
fiatRates: Map<Currency, BigDecimal>,
rateFormatter: (BigDecimal) -> String,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val newWalletsData = fiatRates.mapNotNull { (currency, rate) ->
val walletStore = state.getWalletStore(currency) ?: return state
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return state
val walletStore = state.getWalletStore(currency) ?: return state
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return state
val fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token ->
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
val fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token ->
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.symbol)
state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount
),
fiatRate = rate,
fiatRateString = rateFormatter(rate)
)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency)
val newWalletData = state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount
),
fiatRate = rate, fiatRateString = rateFormatted
)
return state.updateWalletData(newWalletData)
return state
.updateWalletsData(newWalletsData)
}
private fun setSingeWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: Currency,
appCurrency: FiatCurrencyName, state: WalletState
private fun setSingleWalletFiatRate(
rate: BigDecimal,
rateFormatted: String,
currency: Currency,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val wallet = state.primaryWalletManager?.wallet ?: return state
val token = wallet.getFirstToken()
if (currency == state.primaryWallet?.currency) {
val fiatAmount = wallet.amounts[AmountType.Coin]?.value
?.toFiatString(rate, appCurrency)
?.toFiatString(rate, appCurrency.code)
val walletData = state.primaryWallet.copy(
currencyData = state.primaryWallet.currencyData.copy(fiatAmountFormatted = fiatAmount),
fiatRate = rate,
@ -433,7 +455,9 @@ private fun setSingeWalletFiatRate(
)
return state.updateWalletData(walletData)
} else if (currency is Currency.Token && currency.token == token) {
val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency)
val tokenFiatAmount = wallet.getTokenAmount(token)
?.value
?.toFiatString(rate, appCurrency.code)
val tokenData = state.primaryWallet?.currencyData?.token?.copy(
fiatAmount = tokenFiatAmount,
fiatRate = rate,

View file

@ -1,8 +1,11 @@
package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.os.Bundle
import android.view.*
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorRes
@ -13,18 +16,30 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.common.extensions.guard
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.extensions.appendIfNotNull
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
import com.tangem.tap.common.extensions.getColor
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.common.extensions.toQrCode
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.getQRReceiveMessage
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
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.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
import com.tangem.tap.store
import com.tangem.wallet.R
@ -35,7 +50,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
StoreSubscriber<WalletState> {
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private var dialog: Dialog? = null
private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter
private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind)
@ -72,6 +87,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
setupTransactionsRecyclerView()
setupButtons()
setupWarningsRecyclerView()
setupTestActionButton()
}
@ -81,15 +97,26 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
rvPendingTransaction.adapter = pendingTransactionAdapter
}
private fun setupWarningsRecyclerView() = with(binding) {
warningMessagesAdapter = WalletDetailWarningMessagesAdapter()
rvWarningMessages.layoutManager = LinearLayoutManager(requireContext())
rvWarningMessages.adapter = warningMessagesAdapter
rvWarningMessages.addItemDecoration(SpaceItemDecoration.vertical(8f))
}
private fun setupButtons() = with(binding) {
btnConfirm.text = getString(R.string.wallet_button_send)
btnConfirm.setOnClickListener { store.dispatch(WalletAction.Send()) }
binding.lWalletDetails.btnShare.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
btnTrade.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) }
btnSell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
rowButtons.onBuyClick = {
store.dispatch(WalletAction.TradeCryptoAction.Buy)
}
rowButtons.onSellClick = {
store.dispatch(WalletAction.TradeCryptoAction.Sell)
}
rowButtons.onTradeClick = {
store.dispatch(WalletAction.DialogAction.ChooseTradeActionDialog)
}
rowButtons.onSendClick = {
store.dispatch(WalletAction.Send())
}
}
private fun setupTestActionButton() {
@ -114,10 +141,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
setupBalanceData(selectedWallet.currencyData)
setupButtons(selectedWallet)
handleDialogs(state.walletDialog)
handleCurrencyIcon(selectedWallet)
handleWalletRent(selectedWallet.warningRent)
handleNotEnoughFundsOnMainCurrency(selectedWallet)
handleWarnings(selectedWallet)
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
@ -147,7 +172,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
private fun setupButtons(selectedWallet: WalletData) = with(binding) {
btnConfirm.isEnabled = selectedWallet.mainButton.enabled
lWalletDetails.btnCopy.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, requireContext()))
@ -158,35 +182,20 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
store.dispatch(WalletAction.ShareAddress(addressString, requireContext()))
}
}
btnTrade.isEnabled = selectedWallet.tradeCryptoState.buyingAllowed
btnSell.show(selectedWallet.tradeCryptoState.sellingAllowed)
}
private fun handleWalletRent(rent: WalletRent?) = with(binding) {
val rent = rent.guard {
lWarning.root.hide()
return
}
val warningMessage = requireContext().getString(
R.string.solana_rent_warning, rent.minRentValue, rent.rentExemptValue
rowButtons.updateButtonsVisibility(
buyAllowed = selectedWallet.tradeCryptoState.buyingAllowed,
sellAllowed = selectedWallet.tradeCryptoState.sellingAllowed,
sendAllowed = selectedWallet.mainButton.enabled,
)
lWarning.tvWarningMessage.text = warningMessage
lWarning.root.show()
}
private fun handleNotEnoughFundsOnMainCurrency(selectedWalletData: WalletData) = with(binding) {
if (selectedWalletData.currency.isBlockchain()) return@with
private fun handleWarnings(selectedWallet: WalletData) = with(binding) {
val converter = WalletWarningConverter(requireContext())
val warningDetails = selectedWallet.assembleWarnings().map { converter.convert(it) }
if (selectedWalletData.shouldShowCoinAmountWarning()) {
val blockchainName = selectedWalletData.currency.blockchain.fullName
val warningMessage = requireContext().getString(
R.string.token_details_send_blocked_fee_format, blockchainName, blockchainName
)
lWarning.tvWarningMessage.text = warningMessage
lWarning.root.show()
} else {
lWarning.root.hide()
}
warningMessagesAdapter.submitList(warningDetails)
rvWarningMessages.show(warningDetails.isNotEmpty())
}
private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) {
@ -299,24 +308,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
)
}
}
binding.cardPendingTransactionWarning.show(data.status == BalanceStatus.SameCurrencyTransactionInProgress)
}
private fun handleDialogs(walletDialog: StateDialog?) {
when (walletDialog) {
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply {
this.show(walletDialog.amounts)
}
}
null -> {
dialog?.dismiss()
dialog = null
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_remove -> {

View file

@ -13,9 +13,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -24,8 +24,11 @@ import com.tangem.tap.domain.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
@ -79,7 +82,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(WalletAction.Scan)
}
setupWarningsRecyclerView()
walletView.changeWalletView(this, binding)
@ -100,7 +103,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
with(binding) {
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt()))
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
}
}
@ -179,8 +182,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
CardTou(),
store.state.globalState.appCurrency,
tangemTechService = store.state.domainNetworks.tangemTechService
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true

View file

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

View file

@ -63,7 +63,7 @@ class PendingTransactionsAdapter
}
binding.tvPendingTransaction.text = binding.root.getString(transactionDescriptionRes)
transaction.amountUi?.let { binding.tvPendingTransactionAmount.text = "$it " }
transaction.amountValueUi?.let { binding.tvPendingTransactionAmount.text = "$it " }
binding.tvPendingTransactionCurrency.text = "${transaction.currency}"
if (transaction.address != null) {

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@ -62,67 +63,63 @@ class WalletAdapter
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() }
tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted
tvExchangeRate.text = wallet.fiatRateString
cardWallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
val status = wallet.currencyData.status
val isCustomCurrency = wallet.currency.isCustomCurrency(
derivationStyle = store.state.globalState
.scanResponse
?.card
?.derivationStyle
)
val statusMessage = when (status) {
BalanceStatus.TransactionInProgress -> {
root.getString(R.string.wallet_balance_tx_in_progress)
}
BalanceStatus.Unreachable -> {
root.getString(R.string.wallet_balance_blockchain_unreachable)
}
BalanceStatus.NoAccount -> {
root.getString(R.string.wallet_error_no_account)
}
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain,
BalanceStatus.Loading,
null -> null
}
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)
if (status == null || status == BalanceStatus.Loading) {
lContent.root.hide()
lShimmer.root.veil()
} else {
lShimmer.root.unVeil()
lContent.root.show()
}
Picasso.get().loadCurrenciesIcon(
imageView = ivCurrency,
textView = tvTokenLetter,
token = token, blockchain = blockchain,
token = (wallet.currency as? Currency.Token)?.token,
blockchain = wallet.currency.blockchain,
)
when (wallet.currencyData.status) {
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning(isCustom)
BalanceStatus.Loading -> {
hideWarning(isCustom)
if (wallet.currencyData.amountFormatted == null) {
tvExchangeRate.text = root.getString(R.string.wallet_balance_loading)
}
}
BalanceStatus.TransactionInProgress ->
showWarning(root.getString(R.string.wallet_balance_tx_in_progress), isCustom)
BalanceStatus.Unreachable ->
showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable), isCustom)
lContent.tvCurrency.text = wallet.currencyData.currency
lContent.tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted ?: ""
lContent.tvAmount.text = wallet.currencyData.amountFormatted ?: ""
BalanceStatus.NoAccount ->
showWarning(root.getString(R.string.wallet_error_no_account), isCustom)
else -> {
}
}
}
lContent.tvStatus.isVisible = statusMessage != null
lContent.tvStatus.text = statusMessage
private fun showWarning(message: String, isCustom: Boolean = false) {
toggleWarning(true, isCustom)
binding.tvStatusErrorMessage.text = message
}
private fun hideWarning(isCustom: Boolean = false) {
toggleWarning(false, isCustom)
}
private fun toggleWarning(show: Boolean, isCustom: Boolean = false) {
if (!show) {
binding.tvExchangeRate.show(!isCustom)
binding.tvCustomCurrency.show(isCustom)
lContent.tvExchangeRate.isVisible = statusMessage == null
lContent.tvExchangeRate.text = if (isCustomCurrency) {
root.getString(id = R.string.token_item_no_rate)
} else {
binding.tvExchangeRate.hide()
binding.tvCustomCurrency.hide()
wallet.fiatRateString
}
cardWallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
}
binding.tvStatusErrorMessage.show(show)
}
}
}

View file

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

View file

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

View file

@ -1,22 +1,29 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.blockchain.common.Amount
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.DialogWalletSendBinding
import com.tangem.wallet.databinding.ItemWalletAmountToSendBinding
class AmountToSendDialog(context: Context) : BottomSheetDialog(context) {
class AmountToSendBottomSheetDialog(
context: Context,
private val dialog: WalletDialog.SelectAmountToSendDialog,
) : BottomSheetDialog(context) {
var binding: DialogWalletSendBinding? = null
@ -26,29 +33,25 @@ class AmountToSendDialog(context: Context) : BottomSheetDialog(context) {
setContentView(binding!!.root)
}
override fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) {
super.setOnCancelListener(listener)
binding = null
}
override fun show() {
super.show()
fun show(amounts: List<Amount>?) {
this.setOnDismissListener {
store.dispatch(WalletAction.Send.Cancel)
setOnDismissListener {
binding = null
store.dispatch(WalletAction.DialogAction.Hide)
}
binding!!.rvAmountsToSend.layoutManager = LinearLayoutManager(context)
val dividerItemDecoration = DividerItemDecoration(
ContextThemeWrapper(binding!!.root.context, R.style.AppTheme),
DividerItemDecoration.VERTICAL
ContextThemeWrapper(binding!!.root.context, R.style.AppTheme),
DividerItemDecoration.VERTICAL
)
binding!!.rvAmountsToSend.addItemDecoration(dividerItemDecoration)
val viewAdapter = ChooseAmountAdapter()
binding!!.rvAmountsToSend.adapter = viewAdapter
viewAdapter.submitList(amounts)
show()
viewAdapter.submitList(dialog.amounts)
}
}
@ -69,22 +72,22 @@ class ChooseAmountAdapter
object DiffUtilCallback : DiffUtil.ItemCallback<Amount>() {
override fun areContentsTheSame(
oldItem: Amount, newItem: Amount
oldItem: Amount, newItem: Amount
) = oldItem.currencySymbol == newItem.currencySymbol
override fun areItemsTheSame(
oldItem: Amount, newItem: Amount
oldItem: Amount, newItem: Amount
) = oldItem == newItem
}
class AmountViewHolder(val binding: ItemWalletAmountToSendBinding) :
RecyclerView.ViewHolder(binding.root) {
RecyclerView.ViewHolder(binding.root) {
fun bind(amount: Amount) = with(binding) {
tvCurrencySymbol.text = amount.currencySymbol
tvAmount.text = amount.value?.toFormattedString(amount.decimals)
root.setOnClickListener {
store.dispatch(WalletAction.Send.Cancel)
store.dispatch(WalletAction.DialogAction.Hide)
store.dispatch(WalletAction.Send(amount))
}
}

View file

@ -8,7 +8,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogWalletTradeBinding
class ChooseTradeActionDialog(context: Context) : BottomSheetDialog(context) {
class ChooseTradeActionBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
var binding: DialogWalletTradeBinding? = null
@ -23,7 +23,7 @@ class ChooseTradeActionDialog(context: Context) : BottomSheetDialog(context) {
this.setOnDismissListener {
binding = null
store.dispatch(WalletAction.HideDialog)
store.dispatch(WalletAction.DialogAction.Hide)
}
binding!!.dialogBtnBuy.setOnClickListener {

View file

@ -24,7 +24,7 @@ class SignedHashesWarningDialog {
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener {
store.dispatch(WalletAction.HideDialog)
store.dispatch(WalletAction.DialogAction.Hide)
}
}.create()
}

View file

@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.TestAction
import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import java.math.BigDecimal

View file

@ -0,0 +1,47 @@
package com.tangem.tap.features.wallet.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.view.isVisible
import com.tangem.wallet.databinding.ViewWalletDetailsButtonsRowBinding
internal class WalletDetailsButtonsRow @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding = ViewWalletDetailsButtonsRowBinding.inflate(
LayoutInflater.from(context),
this
)
var onBuyClick: (() -> Unit)? = null
var onSellClick: (() -> Unit)? = null
var onTradeClick: (() -> Unit)? = null
var onSendClick: (() -> Unit)? = null
init {
orientation = HORIZONTAL
with(binding) {
btnBuy.setOnClickListener { onBuyClick?.invoke() }
btnSell.setOnClickListener { onSellClick?.invoke() }
btnTrade.setOnClickListener { onTradeClick?.invoke() }
btnSend.setOnClickListener { onSendClick?.invoke() }
}
}
fun updateButtonsVisibility(
buyAllowed: Boolean,
sellAllowed: Boolean,
sendAllowed: Boolean
) = with(binding) {
btnBuy.isVisible = (buyAllowed && !sellAllowed) || (!buyAllowed && !sellAllowed)
btnBuy.isEnabled = buyAllowed
btnSell.isVisible = !buyAllowed && sellAllowed
btnTrade.isVisible = buyAllowed && sellAllowed
btnSend.isEnabled = sendAllowed
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
object CurrencySelectionDialog {
fun create(
dialog: WalletDialog.CurrencySelectionDialog,
context: Context
): AlertDialog {
val currenciesToShow = dialog.currenciesList
.map { it.displayName }
.toTypedArray()
var currentSelection = dialog.currenciesList
.indexOfFirst { it.code == dialog.currentAppCurrency.code }
return AlertDialog.Builder(context)
.setTitle(context.getString(R.string.details_row_title_currency))
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> /* no-op */ }
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
dialog.currenciesList.getOrNull(currentSelection)?.let { selectedCurrency ->
store.dispatch(
WalletAction.AppCurrencyAction.SelectAppCurrency(
fiatCurrency = selectedCurrency
)
)
}
}
.setOnDismissListener {
store.dispatchDialogHide()
}
.setSingleChoiceItems(currenciesToShow, currentSelection) { _, which ->
currentSelection = which
}
.create()
}
}

View file

@ -1,24 +1,24 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.app.Dialog
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.common.card.Card
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.hide
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.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
@ -28,7 +28,6 @@ class MultiWalletView : WalletView {
private var fragment: WalletFragment? = null
private var binding: FragmentWalletBinding? = null
private var dialog: Dialog? = null
private lateinit var walletsAdapter: WalletAdapter
@ -37,7 +36,6 @@ class MultiWalletView : WalletView {
setFragment(fragment, binding)
onViewCreated()
showMultiWalletView(binding)
setupButtons(binding)
}
@ -48,7 +46,6 @@ class MultiWalletView : WalletView {
lAddress.root.hide()
lButtonsShort.root.hide()
lButtonsLong.root.hide()
btnScanMultiwallet.show()
rvMultiwallet.show()
btnAddToken.show()
setupWalletCardNumber(binding)
@ -66,11 +63,6 @@ class MultiWalletView : WalletView {
}
}
private fun setupButtons(binding: FragmentWalletBinding) = with(binding) {
btnScanMultiwallet.setOnClickListener { store.dispatch(WalletAction.Scan) }
}
override fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) {
this.fragment = fragment
this.binding = binding
@ -97,6 +89,7 @@ class MultiWalletView : WalletView {
val fragment = fragment ?: return
val binding = binding ?: return
handleTotalBalance(binding, state.totalBalance)
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
binding.btnAddToken.setOnClickListener {
@ -124,7 +117,33 @@ class MultiWalletView : WalletView {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)
handleDialogs(state.walletDialog)
}
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,
) = with(binding.lCardTotalBalance) {
root.isVisible = totalBalance != null
if (totalBalance != null) {
if (totalBalance.state == TotalBalance.State.Loading) {
veilBalance.veil()
} else {
veilBalance.unVeil()
}
tvProcessing.animateVisibility(
show = totalBalance.state == TotalBalance.State.SomeTokensFailed
)
tvBalance.text = if (totalBalance.state == TotalBalance.State.SomeTokensFailed) ""
else totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol
)
tvCurrencyName.text = totalBalance.fiatCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
}
}
private fun handleErrorStates(
@ -148,6 +167,8 @@ class MultiWalletView : WalletView {
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle)
)
}
else -> { /* no-op */
}
}
}
@ -167,27 +188,10 @@ class MultiWalletView : WalletView {
private fun configureButtonsForEmptyWalletState(binding: FragmentWalletBinding) =
with(binding) {
btnScanMultiwallet.hide()
lButtonsLong.root.show()
lButtonsLong.btnScanLong.setOnClickListener { store.dispatch(WalletAction.Scan) }
lButtonsLong.btnConfirmLong.setOnClickListener { store.dispatch(WalletAction.CreateWallet) }
lButtonsLong.btnConfirmLong.text =
fragment?.getText(R.string.wallet_button_create_wallet)
}
private fun handleDialogs(walletDialog: StateDialog?) {
val fragment = fragment ?: return
val context = fragment.context ?: return
when (walletDialog) {
is WalletDialog.SignedHashesMultiWalletDialog -> {
if (dialog == null) {
dialog = SignedHashesWarningDialog.create(context).apply { show() }
}
}
else -> {
dialog?.dismiss()
dialog = null
}
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.app.Dialog
import android.view.View
import android.view.ViewGroup
import android.widget.Button
@ -10,17 +9,18 @@ import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.TradeCryptoState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
@ -30,7 +30,6 @@ class SingleWalletView : WalletView {
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private var fragment: WalletFragment? = null
private var binding: FragmentWalletBinding? = null
private var dialog: Dialog? = null
override fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) {
this.fragment = fragment
@ -51,7 +50,6 @@ class SingleWalletView : WalletView {
private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) {
rvMultiwallet.hide()
btnAddToken.hide()
btnScanMultiwallet.hide()
rvPendingTransaction.hide()
lCardBalance.root.show()
lAddress.root.show()
@ -80,7 +78,6 @@ class SingleWalletView : WalletView {
setupAddressCard(state.primaryWallet, binding)
showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions)
setupBalance(state, state.primaryWallet)
handleDialogs(state.walletDialog, fragment)
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
@ -153,7 +150,14 @@ class SingleWalletView : WalletView {
}
}
lAddress.btnShowQr.setOnClickListener {
store.dispatch(WalletAction.ShowDialog.QrCode)
state.walletAddresses?.selectedAddress?.let { selectedAddress ->
store.dispatch(
WalletAction.DialogAction.QrCode(
currency = state.currency,
selectedAddress = selectedAddress
)
)
}
}
setupTradeButton(binding, state.tradeCryptoState)
@ -165,7 +169,7 @@ class SingleWalletView : WalletView {
val action = when {
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy
!allowedToBuy && allowedToSell -> WalletAction.TradeCryptoAction.Sell
allowedToBuy && allowedToSell -> WalletAction.ShowDialog.ChooseTradeActionDialog
allowedToBuy && allowedToSell -> WalletAction.DialogAction.ChooseTradeActionDialog
else -> null
}
val text = when {
@ -257,28 +261,4 @@ class SingleWalletView : WalletView {
binding.lAddress.root.hide()
}
}
private fun handleDialogs(walletDialog: StateDialog?, fragment: WalletFragment) {
val context = fragment.context ?: return
when (walletDialog) {
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(context).apply {
this.show(walletDialog.amounts)
}
}
is WalletDialog.SignedHashesMultiWalletDialog -> {
if (dialog == null) {
dialog = SignedHashesWarningDialog.create(context).apply { show() }
}
}
is WalletDialog.ChooseTradeActionDialog -> {
if (dialog == null) dialog = ChooseTradeActionDialog(context).apply { show() }
}
null -> {
dialog?.dismiss()
dialog = null
}
}
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.network.coinmarketcap
import com.tangem.network.common.AddHeaderInterceptor
import com.tangem.network.common.createRetrofitInstance
import retrofit2.http.GET
import retrofit2.http.Query
interface CoinMarketCapApi {
@GET("v1/tools/price-conversion")
suspend fun getRateInfo(
@Query("amount") amount: Int,
@Query("symbol") cryptoCurrencyName: String,
@Query("convert") fiatCurrencyName: String? = null
): RateInfoResponse
@GET("v1/fiat/map")
suspend fun getFiatMap(): FiatMapResponse
companion object {
private const val baseUrl = "https://pro-api.coinmarketcap.com/"
fun create(apiKey: String): CoinMarketCapApi {
return createRetrofitInstance(
baseUrl = baseUrl,
interceptors = listOf(
AddHeaderInterceptor(mapOf("X-CMC_PRO_API_KEY" to apiKey)),
),
).create(CoinMarketCapApi::class.java)
}
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.network.coinmarketcap
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
//TODO: refactoring: move to the domain module and aggregate it as the alternative service for TangemTech
class CoinMarketCapService() {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create(getApiKey()) }
private fun getApiKey(): String {
return store.state.globalState.configManager?.config?.coinMarketCapKey ?: ""
}
suspend fun getRate(
currency: String, fiatCurrency: FiatCurrencyName? = null
): Result<BigDecimal> = withContext(Dispatchers.IO) {
val response = performRequest { api.getRateInfo(1, currency, fiatCurrency) }
return@withContext when (response) {
is Result.Success -> Result.Success(response.data.data.getRate())
is Result.Failure -> response
}
}
suspend fun getFiatCurrencies(): Result<List<FiatCurrency>> = withContext(Dispatchers.IO) {
val response = performRequest { api.getFiatMap() }
return@withContext when (response) {
is Result.Success -> Result.Success(response.data.data.sortedBy { it.name })
is Result.Failure -> response
}
}
}

View file

@ -1,51 +0,0 @@
package com.tangem.tap.network.coinmarketcap
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
class RateInfoResponse : CoinMarketResponse<RateData>()
@JsonClass(generateAdapter = true)
data class RateData(
val quote: Map<String, CurrencyRate>
) {
fun getRate(): BigDecimal = quote.values.first().price
}
@JsonClass(generateAdapter = true)
data class CurrencyRate(
val price: BigDecimal
)
@JsonClass(generateAdapter = true)
data class Status(
val timestamp: String,
@Json(name = "error_code")
val errorCode: Int,
@Json(name = "error_message")
val errorMessage: String?,
val elapsed: Int,
@Json(name = "credit_count")
val creditCount: Int,
val notice: String?
)
@JsonClass(generateAdapter = true)
class FiatMapResponse : CoinMarketResponse<List<FiatCurrency>>()
@JsonClass(generateAdapter = true)
open class CoinMarketResponse<T : Any> {
lateinit var status: Status
lateinit var data: T
}
@JsonClass(generateAdapter = true)
data class FiatCurrency(
val id: Int,
val name: String,
val sign: String,
val symbol: String
)

View file

@ -30,7 +30,7 @@ interface ExchangeUrlBuilder {
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrency: String,
fiatCurrencyName: String,
walletAddress: String,
): String?
@ -74,13 +74,19 @@ class CurrencyExchangeManager(
action: Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrency: String,
fiatCurrencyName: String,
walletAddress: String,
): String? {
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrency, walletAddress)
return urlBuilder.getUrl(
action,
blockchain,
cryptoCurrencyName,
fiatCurrencyName,
walletAddress
)
}
override fun getSellCryptoReceiptUrl(action: Action, transactionId: String): String? {

View file

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.entities.FiatCurrency
/**
[REDACTED_AUTHOR]
@ -12,15 +13,25 @@ class FiatCurrenciesPrefStorage(
private val preferences: SharedPreferences,
private val converter: MoshiJsonConverter,
) {
private val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
private val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
fun migrate() {
preferences.edit(true) {
remove(FIAT_CURRENCIES_KEY_OLD)
remove(APP_CURRENCY_KEY_OLD)
}
}
fun getAppCurrency(): FiatCurrency {
val json = preferences.getString(APP_CURRENCY_KEY, "")
if (json.isNullOrBlank()) return FiatCurrency.Default
return converter.fromJson(json) ?: FiatCurrency.Default
}
fun saveAppCurrency(fiatCurrency: FiatCurrency) {
val json = converter.toJson(fiatCurrency)
preferences.edit { putString(APP_CURRENCY_KEY, json) }
}
fun save(currencies: List<CurrenciesResponse.Currency>) {
val json: String = converter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
@ -33,4 +44,12 @@ class FiatCurrenciesPrefStorage(
return converter.fromJson(json, type) ?: emptyList()
}
companion object {
private const val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
private const val APP_CURRENCY_KEY_OLD = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
private const val APP_CURRENCY_KEY = "appCurrency_v2"
}
}

View file

@ -5,8 +5,6 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import java.util.*
@ -27,15 +25,6 @@ class PreferencesStorage(applicationContext: Application) {
fiatCurrenciesPrefStorage.migrate()
}
fun getAppCurrency(): FiatCurrencyName {
return preferences.getString(APP_CURRENCY_KEY, DEFAULT_FIAT_CURRENCY)
?: DEFAULT_FIAT_CURRENCY
}
fun saveAppCurrency(fiatCurrencyName: FiatCurrencyName) {
preferences.edit { putString(APP_CURRENCY_KEY, fiatCurrencyName) }
}
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
@Deprecated("Use UsedCardsPrefStorage instead")
@ -74,7 +63,6 @@ class PreferencesStorage(applicationContext: Application) {
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"

View file

@ -0,0 +1,13 @@
<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">
<group>
<clip-path android:pathData="M0,0h22v22h-22z"/>
<path android:fillColor="#F1F1F1" android:pathData="M11,22C17.1327,22 22,17.0354 22,11C22,4.8673 17.0354,0 11,0C4.8673,0 0,4.9646 0,11C0,17.0354 4.9646,22 11,22Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M16.5486,12.9469L13.6283,8.469L12.5575,10.2212L15.3805,14.7964L16.646,14.2124L16.5486,12.9469Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M15.186,15.3805L12.2656,10.9026L11.0001,12.8495L13.3364,16.5486L14.7966,15.9645L15.186,15.3805Z"/>
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M5.6461,7.8851L11.0001,4.7701L16.3541,7.8851V14.1152L11.0001,17.2303L7.0089,14.9913L7,16L10.9028,18.3984C11.0001,18.4958 11.0974,18.4958 11.1948,18.3984L17.3275,14.894C17.4249,14.7966 17.5222,14.6993 17.5222,14.602V7.5931C17.5222,7.4958 17.4249,7.3984 17.3275,7.3011L11.1948,3.7966C11.0974,3.6993 11.0001,3.6993 10.9028,3.7966L4.77,7.2037C4.6727,7.3011 4.5753,7.3984 4.5753,7.4958V14.5046C4.5753,14.602 4.6727,14.6993 4.77,14.7966L6,15.5L6.5,14.5L5.7435,14.1152V7.8851H5.6461Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M13.3364,7.96H11.2921L6.5222,15.7476L8.1771,16.721L13.3364,7.96Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M8.1771,8.6636C8.6638,7.8849 10.0266,7.8849 10.6107,7.9822L6,15.5C5.8053,15.4027 5.3541,15.1857 5.062,14.9911C4.6727,14.7964 4.5753,14.7964 4.5753,14.5043V14.1149C5.6461,12.5574 7.6904,9.4424 8.1771,8.6636Z"/>
</group>
</vector>

View file

@ -0,0 +1,13 @@
<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">
<group>
<clip-path android:pathData="M0,0h22v22h-22z"/>
<path android:fillColor="#2C374B" android:pathData="M11,22C17.1327,22 22,17.0354 22,11C22,4.8673 17.0354,0 11,0C4.8673,0 0,4.9646 0,11C0,17.0354 4.9646,22 11,22Z"/>
<path android:fillColor="#24A2EE" android:pathData="M16.5486,12.9469L13.6283,8.469L12.5575,10.2212L15.3805,14.7964L16.646,14.2124L16.5486,12.9469Z"/>
<path android:fillColor="#24A2EE" android:pathData="M15.186,15.3805L12.2656,10.9026L11.0001,12.8495L13.3364,16.5486L14.7966,15.9645L15.186,15.3805Z"/>
<path android:fillColor="#98BCDC" android:fillType="evenOdd" android:pathData="M5.6461,7.8851L11.0001,4.7701L16.3541,7.8851V14.1152L11.0001,17.2303L7.0089,14.9913L7,16L10.9028,18.3984C11.0001,18.4958 11.0974,18.4958 11.1948,18.3984L17.3275,14.894C17.4249,14.7966 17.5222,14.6993 17.5222,14.602V7.5931C17.5222,7.4958 17.4249,7.3984 17.3275,7.3011L11.1948,3.7966C11.0974,3.6993 11.0001,3.6993 10.9028,3.7966L4.77,7.2037C4.6727,7.3011 4.5753,7.3984 4.5753,7.4958V14.5046C4.5753,14.602 4.6727,14.6993 4.77,14.7966L6,15.5L6.5,14.5L5.7435,14.1152V7.8851H5.6461Z"/>
<path android:fillColor="#ffffff" android:pathData="M13.3364,7.96H11.2921L6.5222,15.7476L8.1771,16.721L13.3364,7.96Z"/>
<path android:fillColor="#ffffff" android:pathData="M8.1771,8.6636C8.6638,7.8849 10.0266,7.8849 10.6107,7.9822L6,15.5C5.8053,15.4027 5.3541,15.1857 5.062,14.9911C4.6727,14.7964 4.5753,14.7964 4.5753,14.5043V14.1149C5.6461,12.5574 7.6904,9.4424 8.1771,8.6636Z"/>
</group>
</vector>

View file

@ -1,20 +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"/>
<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"/>
<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"/>
<clip-path android:pathData="M11.0001,3.8501L11.0001,3.8501A4.4,4.4 0,0 1,15.4001 8.2501L15.4001,13.7501A4.4,4.4 0,0 1,11.0001 18.1501L11.0001,18.1501A4.4,4.4 0,0 1,6.6001 13.7501L6.6001,8.2501A4.4,4.4 0,0 1,11.0001 3.8501z"/>
<path android:fillColor="#343434" android:pathData="M10.999,3.8501L10.903,4.1759V13.6292L10.999,13.7249L15.3977,11.1311L10.999,3.8501Z"/>
<path android:fillColor="#8C8C8C" android:pathData="M10.9988,3.8501L6.6001,11.1311L10.9988,13.725V9.1366V3.8501Z"/>
<path android:fillColor="#3C3C3B" android:pathData="M10.9987,14.5559L10.9446,14.6217V17.9892L10.9987,18.147L15.4001,11.9634L10.9987,14.5559Z"/>
<path android:fillColor="#8C8C8C" android:pathData="M10.9988,18.147V14.5559L6.6001,11.9634L10.9988,18.147Z"/>
<path android:fillColor="#141414" android:pathData="M10.9987,13.725L15.3973,11.1312L10.9987,9.1367V13.725Z"/>
<path android:fillColor="#393939" android:pathData="M6.6001,11.1312L10.9987,13.7251V9.1367L6.6001,11.1312Z"/>
</group>
</vector>

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