Updated on 2026-08-14
This commit is contained in:
commit
b804a4e7a0
141 changed files with 3454 additions and 1711 deletions
|
|
@ -4,6 +4,11 @@ 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",
|
||||
]
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
|
|
@ -13,6 +18,8 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
|
|
@ -24,7 +31,8 @@ android {
|
|||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.debug
|
||||
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
|
||||
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
|
||||
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
|
||||
}
|
||||
debug {
|
||||
debuggable true
|
||||
|
|
@ -34,15 +42,18 @@ android {
|
|||
firebaseCrashlytics {
|
||||
mappingFileUploadEnabled false
|
||||
}
|
||||
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"dev\"'
|
||||
buildConfigField 'String', configVar.configEnvironment, '\"dev\"'
|
||||
buildConfigField 'Boolean', configVar.testActionEnabled, 'true'
|
||||
}
|
||||
debug_beta {
|
||||
initWith release
|
||||
debuggable false
|
||||
versionNameSuffix "-beta"
|
||||
applicationIdSuffix ".debug"
|
||||
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
|
||||
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
|
||||
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
|
||||
}
|
||||
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
|
|
@ -69,8 +80,8 @@ repositories {
|
|||
dependencies {
|
||||
implementation fileTree(include: ['*.aar'], dir: 'libs')
|
||||
implementation implementation(project(path: ':domain'))
|
||||
// TODO: refactoring: only for backwards compatibility with non-relocated services to the network module
|
||||
implementation implementation(project(path: ':network'))
|
||||
implementation implementation(project(path: ':common'))
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
|
|
@ -82,7 +93,7 @@ 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-71'
|
||||
implementation 'com.tangem:blockchain:develop-77'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"tokens" : [
|
||||
"coins" : [
|
||||
{
|
||||
"id" : "polygon/test",
|
||||
"id" : "matic-token",
|
||||
"symbol" : "MATIC",
|
||||
"name" : "Polygon",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0x0000000000000000000000000000000000001010",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0x0000000000000000000000000000000000001010",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -16,20 +16,15 @@
|
|||
"id" : "Dai Stablecoin-DAI",
|
||||
"symbol" : "DAI",
|
||||
"name" : "Dai Stablecoin",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
|
||||
"decimalCount" : 18
|
||||
},
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
|
||||
"decimalCount" : 18
|
||||
},
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -38,10 +33,10 @@
|
|||
"id" : "Dummy ERC20-DERC20",
|
||||
"symbol" : "DERC20",
|
||||
"name" : "Dummy ERC20",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -50,15 +45,15 @@
|
|||
"id" : "Ether-ETH",
|
||||
"symbol" : "ETH",
|
||||
"name" : "Ether",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
|
||||
"decimalCount" : 18
|
||||
},
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -67,10 +62,10 @@
|
|||
"id" : "Test Token-TST",
|
||||
"symbol" : "TST",
|
||||
"name" : "Test Token",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -79,20 +74,15 @@
|
|||
"id" : "Tether USD-USDT",
|
||||
"symbol" : "USDT",
|
||||
"name" : "Tether USD",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
|
||||
"decimalCount" : 6
|
||||
},
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
|
||||
"decimalCount" : 18
|
||||
},
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -101,10 +91,10 @@
|
|||
"id" : "Wrapped Ether-WETH",
|
||||
"symbol" : "WETH",
|
||||
"name" : "Wrapped Ether",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -113,30 +103,32 @@
|
|||
"id" : "Wrapped Matic-WMATIC",
|
||||
"symbol" : "WMATIC",
|
||||
"name" : "Wrapped Matic",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polygon-pos",
|
||||
"address" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
|
||||
"networkId" : "polygon-pos/test",
|
||||
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "solana/test",
|
||||
"id" : "solana",
|
||||
"symbol" : "SOL",
|
||||
"name" : "Solana",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "solana/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "Tangem Coin A-TCA",
|
||||
"symbol" : "TCA",
|
||||
"name" : "Tangem Coin A",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "solana",
|
||||
"address" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
|
||||
"networkId" : "solana/test",
|
||||
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
|
||||
"decimalCount" : 9
|
||||
}
|
||||
]
|
||||
|
|
@ -145,50 +137,47 @@
|
|||
"id" : "Tangem Coin B-TCB",
|
||||
"symbol" : "TCB",
|
||||
"name" : "Tangem Coin B",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "solana",
|
||||
"address" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
|
||||
"networkId" : "solana/test",
|
||||
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
|
||||
"decimalCount" : 9
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "binance/test",
|
||||
"id" : "binancecoin",
|
||||
"symbol" : "BNB",
|
||||
"name" : "Binance",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "binancecoin/test"
|
||||
},
|
||||
{
|
||||
"networkId" : "binance-smart-chain/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "Hemster - 452-HEM",
|
||||
"symbol" : "HEM",
|
||||
"name" : "Hemster - 452",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "binancecoin",
|
||||
"address" : "HEM-452",
|
||||
"networkId" : "binancecoin/test",
|
||||
"contractAddress" : "HEM-452",
|
||||
"decimalCount" : 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "bsc/test",
|
||||
"symbol" : "BNB",
|
||||
"name" : "Bsc",
|
||||
"contracts" : [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "Binance-Peg BTCB Token-BTCB",
|
||||
"symbol" : "BTCB",
|
||||
"name" : "Binance-Peg BTCB Token",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -197,10 +186,10 @@
|
|||
"id" : "Binance-Peg BUSD Token-BUSD",
|
||||
"symbol" : "BUSD",
|
||||
"name" : "Binance-Peg BUSD Token",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -209,10 +198,10 @@
|
|||
"id" : "Binance-Peg USDC Token-USDC",
|
||||
"symbol" : "USDC",
|
||||
"name" : "Binance-Peg USDC Token",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0x64544969ed7ebf5f083679233325356ebe738930",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -221,30 +210,32 @@
|
|||
"id" : "Binance-Peg XRP-XRP",
|
||||
"symbol" : "XRP",
|
||||
"name" : "Binance-Peg XRP",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "binance-smart-chain",
|
||||
"address" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
|
||||
"networkId" : "binance-smart-chain/test",
|
||||
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "ethereum/test",
|
||||
"id" : "ethereum",
|
||||
"symbol" : "ETH",
|
||||
"name" : "Ethereum",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "ethereum/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "Weenus-WEENUS",
|
||||
"symbol" : "WEENUS",
|
||||
"name" : "Weenus",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "ethereum",
|
||||
"address" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
|
||||
"networkId" : "ethereum/test",
|
||||
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -253,10 +244,10 @@
|
|||
"id" : "Xeenus-XEENUS",
|
||||
"symbol" : "XEENUS",
|
||||
"name" : "Xeenus",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "ethereum",
|
||||
"address" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
|
||||
"networkId" : "ethereum/test",
|
||||
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -265,10 +256,10 @@
|
|||
"id" : "Yeenus-YEENUS",
|
||||
"symbol" : "YEENUS",
|
||||
"name" : "Yeenus",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "ethereum",
|
||||
"address" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
|
||||
"networkId" : "ethereum/test",
|
||||
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
|
||||
"decimalCount" : 8
|
||||
}
|
||||
]
|
||||
|
|
@ -277,30 +268,32 @@
|
|||
"id" : "Zeenus-ZEENUS",
|
||||
"symbol" : "ZEENUS",
|
||||
"name" : "Zeenus",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "ethereum",
|
||||
"address" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
|
||||
"networkId" : "ethereum/test",
|
||||
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
|
||||
"decimalCount" : 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "avalanche/test",
|
||||
"id" : "avalanche-2",
|
||||
"symbol" : "AVAX",
|
||||
"name" : "Avalanche",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "avalanche/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "The Fuji stablecoin-FUJISTABLE",
|
||||
"symbol" : "FUJISTABLE",
|
||||
"name" : "The Fuji stablecoin",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "avalanche",
|
||||
"address" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
|
||||
"networkId" : "avalanche/test",
|
||||
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
|
||||
"decimalCount" : 6
|
||||
}
|
||||
]
|
||||
|
|
@ -309,30 +302,32 @@
|
|||
"id" : "To the Moon-FUJIMOON",
|
||||
"symbol" : "FUJIMOON",
|
||||
"name" : "To the Moon",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "avalanche",
|
||||
"address" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
|
||||
"networkId" : "avalanche/test",
|
||||
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
|
||||
"decimalCount" : 9
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "fantom/test",
|
||||
"id" : "fantom",
|
||||
"symbol" : "FTM",
|
||||
"name" : "Fantom",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "fantom/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "Fantom USD-FUSD",
|
||||
"symbol" : "FUSD",
|
||||
"name" : "Fantom USD",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "fantom",
|
||||
"address" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
|
||||
"networkId" : "fantom/test",
|
||||
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
|
|
@ -341,29 +336,36 @@
|
|||
"id" : "Wrapped Fantom-WFTM",
|
||||
"symbol" : "WFTM",
|
||||
"name" : "Wrapped Fantom",
|
||||
"contracts" : [
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "fantom",
|
||||
"address" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
|
||||
"networkId" : "fantom/test",
|
||||
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
|
||||
"decimalCount" : 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "bitcoin/test",
|
||||
"id" : "bitcoin",
|
||||
"symbol" : "BTC",
|
||||
"name" : "Bitcoin",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "bitcoin/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "stellar/test",
|
||||
"id" : "stellar",
|
||||
"symbol" : "XLM",
|
||||
"name" : "Stellar",
|
||||
"contracts" : [
|
||||
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "stellar/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"total" : 0,
|
||||
"imageHost" : "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
class CompositionCounter(
|
||||
val id: String,
|
||||
count: Int = 0
|
||||
) {
|
||||
var count: Int = count
|
||||
private set
|
||||
|
||||
fun increase(id: String): CompositionCounter {
|
||||
if (this.id != id) return this
|
||||
|
||||
count += 1
|
||||
return CompositionCounter(id, count)
|
||||
}
|
||||
}
|
||||
|
||||
class CompositionLogger(
|
||||
private val recomposeViewId: String,
|
||||
private val tag: String = recomposeViewId,
|
||||
private var turnOnForIds: List<String> = listOf(recomposeViewId)
|
||||
) {
|
||||
val count: Int
|
||||
get() = compositionCounter.count
|
||||
|
||||
private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId)
|
||||
|
||||
fun nextComposition() {
|
||||
compositionCounter = compositionCounter.increase(recomposeViewId)
|
||||
log("")
|
||||
}
|
||||
|
||||
fun log(message: String) {
|
||||
if (!turnOnForIds.contains(recomposeViewId)) return
|
||||
|
||||
Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message")
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
|
||||
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 WalletConnectDialog.UnsupportedCard ->
|
||||
SimpleAlertDialog.create(
|
||||
|
|
|
|||
75
app/src/main/java/com/tangem/tap/common/TestActions.kt
Normal file
75
app/src/main/java/com/tangem/tap/common/TestActions.kt
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.BuildConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
object TestActions {
|
||||
|
||||
// It used only for the test actions in debug or debug_beta builds
|
||||
var testAmountInjectionForWalletManagerEnabled = false
|
||||
|
||||
/**
|
||||
* @param isTestView - true must be used if you want to show or hide your view depends on BuildConfig
|
||||
*/
|
||||
fun initFor(view: View, actions: List<TestAction>, isTestView: Boolean = false) {
|
||||
if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED)
|
||||
if (!BuildConfig.TEST_ACTION_ENABLED) return
|
||||
|
||||
view.setOnClickListener {
|
||||
store.dispatchDialogShow(AppDialog.TestActionsDialog(actions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias TestAction = Pair<String, () -> Unit>
|
||||
|
||||
class TestActionsBottomSheetDialog(
|
||||
private val appDialog: AppDialog.TestActionsDialog,
|
||||
context: Context
|
||||
) : BottomSheetDialog(context) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(createContentView())
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.peekHeight = 400
|
||||
setOnCancelListener {
|
||||
store.dispatchDialogHide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createContentView(): View {
|
||||
val actionButtons = mutableListOf<View>()
|
||||
appDialog.actionsList.forEach { (actionName, action) ->
|
||||
val view = Button(context).apply {
|
||||
text = actionName
|
||||
setOnClickListener {
|
||||
action()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
actionButtons.add(view)
|
||||
}
|
||||
|
||||
return LinearLayoutCompat(context).apply {
|
||||
orientation = LinearLayoutCompat.VERTICAL
|
||||
actionButtons.forEach { addView(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,13 @@ package com.tangem.tap.common.compose
|
|||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.LocalRippleTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -66,6 +70,7 @@ fun PasteButton(
|
|||
enabled: Boolean = true,
|
||||
dpSize: DpSize = DpSize(40.dp, 40.dp),
|
||||
onClick: () -> Unit,
|
||||
tint: Color? = null,
|
||||
content: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
IconButton(
|
||||
|
|
@ -75,14 +80,60 @@ fun PasteButton(
|
|||
) {
|
||||
when (content) {
|
||||
null -> {
|
||||
val icon = if (enabled) R.drawable.ic_paste else R.drawable.ic_paste_disabled
|
||||
Icon(painterResource(id = icon), contentDescription = "Paste")
|
||||
val tintColor = tint
|
||||
?: colorResource(id = if (enabled) R.color.accent else R.color.accent_disabled)
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_paste),
|
||||
contentDescription = "Paste",
|
||||
tint = tintColor,
|
||||
)
|
||||
}
|
||||
else -> content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
dpSize: DpSize = DpSize(40.dp, 40.dp),
|
||||
onClick: () -> Unit,
|
||||
tint: Color? = null,
|
||||
content: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier.size(dpSize),
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
) {
|
||||
when (content) {
|
||||
null -> {
|
||||
val tintColor = tint
|
||||
?: colorResource(id = if (enabled) R.color.accent else R.color.accent_disabled)
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_clear),
|
||||
contentDescription = "Clear",
|
||||
tint = tintColor,
|
||||
)
|
||||
}
|
||||
else -> content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for disable ripple if button is enable = false
|
||||
*/
|
||||
@Composable
|
||||
fun ToggledRippleTheme(
|
||||
isEnabled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val theme = LocalRippleTheme provides if (isEnabled) LocalRippleTheme.current else NoRippleTheme()
|
||||
CompositionLocalProvider(theme) { content() }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ButtonTest() {
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.tangem.domain.common.util.ValueDebouncer
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> valueDebouncerAsState(
|
||||
debounce: Long = 400,
|
||||
onValueChanged: (T) -> Unit
|
||||
): ValueDebouncer<T> {
|
||||
return remember {
|
||||
ValueDebouncer(
|
||||
debounce = debounce,
|
||||
onValueChanged = { changedValue ->
|
||||
changedValue?.let { onValueChanged(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.tangem.domain.common.util.ValueDebouncer
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> valueDebouncerAsState(
|
||||
initialValue: T,
|
||||
debounce: Long = 600,
|
||||
onEmitValueReceived: (T) -> Unit = {},
|
||||
onValueChanged: (T) -> Unit,
|
||||
): ValueDebouncer<T> {
|
||||
return remember {
|
||||
ValueDebouncer<T>(
|
||||
initialValue = initialValue,
|
||||
debounceDuration = debounce,
|
||||
onEmitValueReceived = { emitValue ->
|
||||
emitValue?.let { onEmitValueReceived(it) }
|
||||
},
|
||||
onValueChanged = { changedValue ->
|
||||
changedValue?.let { onValueChanged(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> valueDebouncerNullableAsState(
|
||||
initialValue: T?,
|
||||
debounce: Long = 400,
|
||||
onEmitValueReceived: (T?) -> Unit = {},
|
||||
onValueChanged: (T?) -> Unit,
|
||||
): ValueDebouncer<T?> {
|
||||
return remember {
|
||||
ValueDebouncer(
|
||||
initialValue = initialValue,
|
||||
debounceDuration = debounce,
|
||||
onEmitValueReceived = onEmitValueReceived,
|
||||
onValueChanged = onValueChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,18 +17,17 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.DomainStateDialog
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
@Composable
|
||||
fun ComposeDialogManager() {
|
||||
val dialogSate = remember { mutableStateOf<DomainStateDialog?>(null) }
|
||||
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
|
||||
val subscriber = remember {
|
||||
object : StoreSubscriber<DomainGlobalState> {
|
||||
override fun newState(state: DomainGlobalState) {
|
||||
|
|
@ -52,17 +51,17 @@ fun ComposeDialogManager() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
|
||||
private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
||||
if (dialogState.value == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { DomainErrorConverter(context) }
|
||||
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
|
||||
|
||||
when (val dialog = dialogState.value) {
|
||||
is DomainDialog.DialogError -> ErrorDialog(
|
||||
title = stringResource(id = R.string.common_error),
|
||||
body = errorConverter.convertError(dialog.error),
|
||||
body = errorConverter.convert(dialog.error),
|
||||
onDismissRequest
|
||||
)
|
||||
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
|
|
@ -30,13 +28,13 @@ fun <T> OutlinedSpinner(
|
|||
onClose: VoidCallback = {}
|
||||
) {
|
||||
val rIsExpanded = remember { mutableStateOf(false) }
|
||||
val rSelectedItem = remember { mutableStateOf(selectedItem.value) }
|
||||
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
|
||||
if (!selectedItem.isUserInput) {
|
||||
rSelectedItem.value = selectedItem.value
|
||||
stateSelectedItem.value = selectedItem.value
|
||||
}
|
||||
|
||||
val onItemSelectedInternal: (T) -> Unit = {
|
||||
rSelectedItem.value = it
|
||||
val onDropDownItemSelectedInternal: (T) -> Unit = {
|
||||
stateSelectedItem.value = it
|
||||
rIsExpanded.value = false
|
||||
onItemSelected(it)
|
||||
}
|
||||
|
|
@ -49,23 +47,23 @@ fun <T> OutlinedSpinner(
|
|||
expanded = rIsExpanded.value,
|
||||
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
|
||||
) {
|
||||
ProvideTextStyle(value = TextStyle(color = Color.Blue)) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
value = textFieldConverter(rSelectedItem.value),
|
||||
onValueChange = {},
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
value = textFieldConverter(stateSelectedItem.value),
|
||||
onValueChange = {},
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
|
||||
)
|
||||
|
||||
if (!isEnabled) return@ExposedDropdownMenuBox
|
||||
ExposedDropdownMenu(
|
||||
expanded = rIsExpanded.value,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
itemList.forEach { item ->
|
||||
DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) {
|
||||
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
|
||||
when (dropdownItemView) {
|
||||
null -> Text(textFieldConverter(item))
|
||||
else -> dropdownItemView(item)
|
||||
|
|
@ -83,7 +81,7 @@ fun TestSpinnerPreview() {
|
|||
OutlinedSpinner(
|
||||
label = "Blockchain name",
|
||||
itemList = listOf(Blockchain.values()),
|
||||
selectedItem = Field.Data(Blockchain.Avalanche),
|
||||
selectedItem = Field.Data(Blockchain.Avalanche, false),
|
||||
onItemSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,18 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.domain.ErrorConverter
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.CompositionLogger
|
||||
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -31,7 +34,7 @@ import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
|||
@Composable
|
||||
fun OutlinedTextFieldWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
textFieldData: Field.Data<String>,
|
||||
fieldData: Field.Data<String>,
|
||||
labelId: Int? = null,
|
||||
label: String = "",
|
||||
placeholderId: Int? = null,
|
||||
|
|
@ -40,8 +43,8 @@ fun OutlinedTextFieldWidget(
|
|||
isEnabled: Boolean = true,
|
||||
isVisible: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
error: DomainError? = null,
|
||||
errorConverter: ErrorConverter<String>? = null,
|
||||
error: ModuleError? = null,
|
||||
errorConverter: ModuleMessageConverter? = null,
|
||||
debounceTextChanges: Long = 400,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
|
|
@ -54,7 +57,7 @@ fun OutlinedTextFieldWidget(
|
|||
) {
|
||||
OutlinedProgressTextField(
|
||||
modifier = modifier,
|
||||
textFieldData = textFieldData,
|
||||
fieldData = fieldData,
|
||||
label = stringResourceDefault(labelId, label),
|
||||
placeholder = stringResourceDefault(placeholderId, placeholder),
|
||||
trailingIcon = trailingIcon,
|
||||
|
|
@ -73,39 +76,91 @@ fun OutlinedTextFieldWidget(
|
|||
@Composable
|
||||
private fun OutlinedProgressTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
textFieldData: Field.Data<String>,
|
||||
fieldData: Field.Data<String>,
|
||||
label: String = "",
|
||||
placeholder: String = "",
|
||||
isEnabled: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
error: DomainError? = null,
|
||||
error: ModuleError? = null,
|
||||
debounce: Long = 400,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
onTextChanged: (String) -> Unit,
|
||||
) {
|
||||
val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged)
|
||||
val rText = remember { mutableStateOf(textFieldData.value) }
|
||||
val logger = remember {
|
||||
CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена"))
|
||||
}
|
||||
logger.nextComposition()
|
||||
|
||||
fun updateFieldValueAndEmmit(value: String) {
|
||||
rText.value = value
|
||||
rTextDebouncer.emmit(value)
|
||||
}
|
||||
// This action came from redux. Update the field value and send a new event as if from the user
|
||||
if (!textFieldData.isUserInput) {
|
||||
updateFieldValueAndEmmit(textFieldData.value)
|
||||
val textValueState = remember { mutableStateOf(fieldData.value) }
|
||||
val textDebouncer = valueDebouncerAsState(
|
||||
initialValue = fieldData.value,
|
||||
debounce = debounce,
|
||||
onEmitValueReceived = {
|
||||
logger.log("DEBOUNCER: onEmitValueReceived: [$it]")
|
||||
logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]")
|
||||
textValueState.value = it
|
||||
},
|
||||
onValueChanged = {
|
||||
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
|
||||
onTextChanged(it)
|
||||
})
|
||||
|
||||
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
|
||||
logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]")
|
||||
logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]")
|
||||
|
||||
if (!fieldData.isUserInput) {
|
||||
// initial value is not from an user
|
||||
val isNotUserInput = "-- IS NOT USER INPUT"
|
||||
logger.log("recompose $isNotUserInput")
|
||||
if (textValueState.value == fieldData.value) {
|
||||
logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле")
|
||||
if ((textDebouncer.emittedValue != textDebouncer.debounced) || textDebouncer.emitsCountBeforeDebounce > 0) {
|
||||
logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные")
|
||||
if (textValueState.value != textDebouncer.emittedValue || textValueState.value != textDebouncer.debounced) {
|
||||
logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer")
|
||||
if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) {
|
||||
logger.log("$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
|
||||
textValueState.value = fieldData.value
|
||||
} else {
|
||||
logger.log("$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
} else {
|
||||
logger.log("$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]")
|
||||
|
||||
Box {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
value = rText.value,
|
||||
onValueChange = ::updateFieldValueAndEmmit,
|
||||
value = textValueState.value,
|
||||
onValueChange = {
|
||||
logger.log("WIDGET: textDebouncer.emmit([$it])")
|
||||
textDebouncer.emmit(it)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
label = { Text(label) },
|
||||
placeholder = { Text(placeholder) },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeholder,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
trailingIcon = trailingIcon,
|
||||
singleLine = true,
|
||||
enabled = isEnabled,
|
||||
|
|
@ -120,13 +175,12 @@ private fun OutlinedProgressTextField(
|
|||
visible = isLoading,
|
||||
) { LinearProgressIndicator() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedErrorView(
|
||||
error: DomainError? = null,
|
||||
errorConverter: ErrorConverter<String>,
|
||||
error: ModuleError? = null,
|
||||
errorConverter: ModuleMessageConverter,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = error != null,
|
||||
|
|
@ -134,7 +188,7 @@ private fun AnimatedErrorView(
|
|||
exit = slideOutVertically() + fadeOut(),
|
||||
) {
|
||||
error?.let {
|
||||
ErrorView(errorConverter.convertError(it), style = TextStyle(fontSize = 14.sp))
|
||||
ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,20 +196,14 @@ private fun AnimatedErrorView(
|
|||
@Preview
|
||||
@Composable
|
||||
fun OutlinedTextFieldWithErrorTest() {
|
||||
val converter = remember {
|
||||
object : ErrorConverter<String> {
|
||||
override fun convertError(error: DomainError): String {
|
||||
return "Hello, i'am the error: ${error::class.java.simpleName}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val converter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
class SimpleError(
|
||||
override val code: Int = 1,
|
||||
override val message: String = "Error message",
|
||||
override val data: Any? = null,
|
||||
) : DomainError
|
||||
) : ModuleError
|
||||
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -166,7 +214,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data(""),
|
||||
fieldData = Field.Data("", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
|
|
@ -174,7 +222,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data("First"),
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
|
|
@ -182,7 +230,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data("First"),
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
isLoading = true,
|
||||
|
|
@ -191,7 +239,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data("First"),
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = SimpleError(),
|
||||
|
|
|
|||
17
app/src/main/java/com/tangem/tap/common/compose/Theme.kt
Normal file
17
app/src/main/java/com/tangem/tap/common/compose/Theme.kt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material.ripple.RippleTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class NoRippleTheme : RippleTheme {
|
||||
@Composable
|
||||
override fun defaultColor() = Color.Unspecified
|
||||
|
||||
@Composable
|
||||
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
|
||||
}
|
||||
53
app/src/main/java/com/tangem/tap/common/compose/Warning.kt
Normal file
53
app/src/main/java/com/tangem/tap/common/compose/Warning.kt
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun AddCustomTokenWarning(
|
||||
modifier: Modifier = Modifier,
|
||||
warning: ModuleMessage,
|
||||
converter: ModuleMessageConverter
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = colorResource(id = R.color.warning_warning),
|
||||
elevation = 4.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_warning),
|
||||
color = colorResource(id = R.color.white),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
text = converter.convert(warning),
|
||||
color = colorResource(id = R.color.white),
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.common.extensions
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.remove
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@DrawableRes
|
||||
|
|
@ -22,6 +23,7 @@ fun Blockchain.getRoundIconRes(): Int {
|
|||
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_round
|
||||
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_round
|
||||
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round
|
||||
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round
|
||||
else -> R.drawable.ic_tangem_logo
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +46,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
|
|||
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_no_color
|
||||
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color
|
||||
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color
|
||||
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color
|
||||
else -> R.drawable.ic_tangem_logo
|
||||
}
|
||||
}
|
||||
|
|
@ -55,4 +58,7 @@ fun Blockchain.getNetworkName(): String {
|
|||
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Blockchain.fullNameWithoutTestnet
|
||||
get() = this.fullName.remove(" Testnet")
|
||||
|
|
@ -26,6 +26,8 @@ import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
|
|||
import com.tangem.tap.features.wallet.ui.WalletFragment
|
||||
import com.tangem.wallet.R
|
||||
|
||||
private class Navigation
|
||||
|
||||
fun FragmentActivity.openFragment(
|
||||
screen: AppScreen,
|
||||
addToBackstack: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
import com.tangem.tap.common.redux.global.FiatCurrencyName
|
||||
import com.tangem.tap.network.coinmarketcap.FiatCurrency
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
|
|
@ -10,7 +10,7 @@ import java.text.DecimalFormatSymbols
|
|||
import java.util.*
|
||||
|
||||
fun BigDecimal.toFormattedString(
|
||||
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
|
||||
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
|
||||
): String {
|
||||
val symbols = DecimalFormatSymbols(locale)
|
||||
val df = DecimalFormat()
|
||||
|
|
@ -26,7 +26,7 @@ fun BigDecimal.toFormattedCurrencyString(
|
|||
decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN,
|
||||
limitNumberOfDecimals: Boolean = true
|
||||
): String {
|
||||
val decimalsForRounding = if (limitNumberOfDecimals){
|
||||
val decimalsForRounding = if (limitNumberOfDecimals) {
|
||||
if (decimals > 8) 8 else decimals
|
||||
} else {
|
||||
decimals
|
||||
|
|
@ -52,7 +52,7 @@ fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: FiatCurrencyName): String
|
|||
return "≈ ${fiatCurrencyName} $this"
|
||||
}
|
||||
|
||||
fun FiatCurrency.toFormattedString(): String = "${this.name} (${this.symbol}) - ${this.sign}"
|
||||
fun CurrenciesResponse.Currency.toFormattedString(): String = "${this.name} (${this.code}) - ${this.unit}"
|
||||
|
||||
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkError
|
|||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.tap.common.TestActions
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.amountToCreateAccount
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
|
|
@ -20,8 +21,9 @@ import timber.log.Timber
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
||||
if (isDemoWallet()) {
|
||||
if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) {
|
||||
delay(500)
|
||||
TestActions.testAmountInjectionForWalletManagerEnabled = false
|
||||
Result.Success(wallet)
|
||||
} else {
|
||||
update()
|
||||
|
|
@ -37,10 +39,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken())
|
||||
|
||||
if (exception is BlockchainSdkError.AccountNotFound && amountToCreateAccount != null) {
|
||||
Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString()))
|
||||
Result.Failure(TapError.WalletManager.NoAccountError(amountToCreateAccount.toString()))
|
||||
} else {
|
||||
val message = exception.localizedMessage ?: "An error has occurred. Try later"
|
||||
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
|
||||
Result.Failure(TapError.WalletManager.InternalError(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.tap.common.moduleMessage
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter
|
||||
|
||||
class ModuleMessageConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<ModuleMessage, String> {
|
||||
|
||||
override fun convert(message: ModuleMessage): String {
|
||||
val convertedMessage = when (message) {
|
||||
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
return convertedMessage ?: convertUnknownMessage(message)
|
||||
}
|
||||
|
||||
private fun convertUnknownMessage(message: ModuleMessage): String {
|
||||
return "Unknown message: ${message::class.java.simpleName}"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.tap.common.moduleMessage.domain
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DomainErrorConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<DomainError, String?> {
|
||||
override fun convert(message: DomainError): String? = when (message) {
|
||||
is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<DomainError, String?> {
|
||||
|
||||
override fun convert(message: DomainError): String? {
|
||||
val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException()
|
||||
|
||||
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.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
|
||||
AddCustomTokenError.InvalidDecimalsCount -> {
|
||||
context.getString(R.string.custom_token_creation_error_wrong_decimals, 30)
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field
|
||||
else -> null
|
||||
}
|
||||
|
||||
return when (rawMessage) {
|
||||
is Int -> context.getString(rawMessage)
|
||||
is String -> rawMessage
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.common.moduleMessage.domain
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DomainMessageConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<DomainModuleMessage, String?> {
|
||||
override fun convert(message: DomainModuleMessage): String? {
|
||||
return when (message) {
|
||||
is DomainError -> DomainErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.common.redux
|
|||
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.NetworkServices
|
||||
import com.tangem.tap.common.redux.global.GlobalMiddleware
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.redux.navigation.NavigationState
|
||||
|
|
@ -54,6 +55,9 @@ data class AppState(
|
|||
val domainState: DomainState
|
||||
get() = domainStore.state
|
||||
|
||||
val domainNetworks: NetworkServices
|
||||
get() = domainState.globalState.networkServices
|
||||
|
||||
companion object {
|
||||
fun getMiddleware(): List<Middleware<AppState>> {
|
||||
return listOf(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import android.content.Context
|
|||
import android.widget.Toast
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.tap.common.extensions.getColor
|
||||
import com.tangem.tap.domain.ArgError
|
||||
import com.tangem.tap.domain.MultiMessageError
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.assembleErrors
|
||||
import com.tangem.tap.notificationsHandler
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import java.lang.ref.WeakReference
|
||||
|
|
@ -29,7 +31,17 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
|
|||
fun showNotification(message: String) {
|
||||
baseLayout.get()?.let { layout ->
|
||||
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
|
||||
.also { snackbar -> snackbar.show() }
|
||||
.also { snackbar -> snackbar.show() }
|
||||
}
|
||||
}
|
||||
|
||||
fun showDebugNotification(message: String) {
|
||||
baseLayout.get()?.let { layout ->
|
||||
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
|
||||
.also { snackbar ->
|
||||
snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning))
|
||||
snackbar.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +63,12 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
|
|||
val message = builder(errorList.map { getMessageString(context, it.first, it.second) })
|
||||
showNotification(message)
|
||||
}
|
||||
|
||||
fun showDebugErrorNotification(message: Int, args: List<Any>? = null) {
|
||||
baseLayout.get()?.let {
|
||||
showDebugNotification(getMessageString(it.context, message, args))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
|
||||
|
|
@ -74,17 +92,37 @@ private fun handleNotificationAction(action: Action) {
|
|||
if (action is Debug && !BuildConfig.DEBUG) return
|
||||
|
||||
when (action) {
|
||||
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
|
||||
is NotificationAction -> {
|
||||
notificationsHandler?.showNotification(action.messageResource)
|
||||
}
|
||||
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
|
||||
is ErrorAction -> {
|
||||
when (action.error) {
|
||||
is MultiMessageError -> {
|
||||
val multiError = action.error as MultiMessageError
|
||||
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
|
||||
when (action) {
|
||||
is Debug -> {
|
||||
val args = (action.error as? ArgError)?.args ?: listOf()
|
||||
when (action) {
|
||||
is DebugNotification -> {
|
||||
notificationsHandler?.showNotification(action.error.messageResource, args)
|
||||
}
|
||||
is DebugToastNotification -> {
|
||||
notificationsHandler?.showToastNotification(action.error.messageResource, args)
|
||||
}
|
||||
is DebugErrorAction -> {
|
||||
notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val args = (action.error as? ArgError)?.args ?: listOf()
|
||||
notificationsHandler?.showNotification(action.error.messageResource, args)
|
||||
when (action.error) {
|
||||
is MultiMessageError -> {
|
||||
val multiError = action.error as MultiMessageError
|
||||
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
|
||||
}
|
||||
else -> {
|
||||
val args = (action.error as? ArgError)?.args ?: listOf()
|
||||
notificationsHandler?.showNotification(action.error.messageResource, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.TestAction
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.Currency
|
||||
|
||||
|
|
@ -17,4 +18,8 @@ sealed class AppDialog : StateDialog {
|
|||
val currency: Currency,
|
||||
val addressData: AddressData,
|
||||
) : AppDialog()
|
||||
|
||||
data class TestActionsDialog(
|
||||
val actionsList: List<TestAction>
|
||||
) : AppDialog()
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -102,12 +100,10 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
|
|||
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
domainStore.dispatch(DomainGlobalAction.SetScanResponse(result.data))
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
|
||||
action.onSuccess?.invoke(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
domainStore.dispatch(DomainGlobalAction.SetScanResponse(null))
|
||||
action.onFailure?.invoke(result.error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.preferencesStorage
|
||||
|
|
@ -33,8 +35,10 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
is GlobalAction.ScanFailsCounter.Reset -> {
|
||||
globalState.copy(scanCardFailsCounter = 0)
|
||||
}
|
||||
is GlobalAction.SaveScanNoteResponse ->
|
||||
is GlobalAction.SaveScanNoteResponse ->{
|
||||
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
|
||||
globalState.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is GlobalAction.ChangeAppCurrency -> {
|
||||
globalState.copy(appCurrency = action.appCurrency)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
|
|||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.StateType
|
||||
|
||||
|
|
@ -20,7 +19,6 @@ data class GlobalState(
|
|||
val cardVerifiedOnline: Boolean = false,
|
||||
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
||||
val payIdManager: PayIdManager = PayIdManager(),
|
||||
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
|
||||
val configManager: ConfigManager? = null,
|
||||
val warningManager: WarningMessagesManager? = null,
|
||||
val feedbackManager: FeedbackManager? = null,
|
||||
|
|
|
|||
83
app/src/main/java/com/tangem/tap/domain/RatesRepository.kt
Normal file
83
app/src/main/java/com/tangem/tap/domain/RatesRepository.kt
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ThrottlerWithValues
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.tap.features.wallet.redux.Currency
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
|
||||
//TODO: refactoring: move to domain
|
||||
class RatesRepository {
|
||||
|
||||
private val tangemTechService: TangemTechService
|
||||
get() = store.state.domainNetworks.tangemTechService
|
||||
|
||||
private val throttler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
|
||||
|
||||
suspend fun loadFiatRate(currencyId: String, coinsList: List<Currency>): Result<RatesResult> {
|
||||
// get and submit previous result of equivalents.
|
||||
val throttledResult = coinsList.filter { throttler.isStillThrottled(it) }.map {
|
||||
Pair(it, throttler.geValue(it))
|
||||
}
|
||||
if (throttledResult.isNotEmpty()) {
|
||||
return handleFiatRatesResult(throttledResult.toMap())
|
||||
}
|
||||
|
||||
val currenciesToUpdate = coinsList.filter { !throttler.isStillThrottled(it) }
|
||||
val coinIds = currenciesToUpdate.mapNotNull { it.coinId }.distinct()
|
||||
if (coinIds.isEmpty()) return EMPTY_RESULT
|
||||
|
||||
return when (val result = tangemTechService.rates(currencyId, coinIds)) {
|
||||
is Result.Success -> {
|
||||
val ratesResultList: Map<String, Result<BigDecimal>> = result.data.rates.mapValues {
|
||||
Result.Success(it.value.toBigDecimal())
|
||||
}
|
||||
val updatedCurrencies = mutableMapOf<Currency, Result<BigDecimal>?>()
|
||||
currenciesToUpdate.forEach { currency ->
|
||||
ratesResultList[currency.coinId]?.let {
|
||||
updatedCurrencies[currency] = it
|
||||
throttler.updateThrottlingTo(currency)
|
||||
throttler.setValue(currency, it)
|
||||
}
|
||||
}
|
||||
handleFiatRatesResult(updatedCurrencies)
|
||||
}
|
||||
is Result.Failure -> Result.Failure(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFiatRatesResult(rates: Map<Currency, Result<BigDecimal>?>): Result.Success<RatesResult> {
|
||||
val success = mutableMapOf<Currency, BigDecimal>()
|
||||
val failures = mutableMapOf<Currency, Throwable>()
|
||||
|
||||
rates.mapNotNull { (currency, priceResult) ->
|
||||
when (priceResult) {
|
||||
is Result.Success -> success[currency] = priceResult.data
|
||||
is Result.Failure -> failures[currency] = priceResult.error
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success(success to failures)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
throttler.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY_RESULT = Result.Success(Pair(
|
||||
mutableMapOf<Currency, BigDecimal>(),
|
||||
mutableMapOf<Currency, Throwable>()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
typealias RatesResult = Pair<MutableMap<Currency, BigDecimal>, MutableMap<Currency, Throwable>>
|
||||
|
||||
val RatesResult.loadedRates
|
||||
get() = this.first
|
||||
|
||||
val RatesResult.failedRates
|
||||
get() = this.second
|
||||
|
|
@ -43,7 +43,8 @@ sealed class TapError(
|
|||
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
|
||||
}
|
||||
|
||||
sealed class WalletManagerUpdate {
|
||||
sealed class WalletManager {
|
||||
object CreationError: CustomError("Can't create wallet manager")
|
||||
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
|
||||
class InternalError(message: String): CustomError(message)
|
||||
object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable)
|
||||
|
|
|
|||
|
|
@ -1,50 +1,40 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.ThrottlerWithValues
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.ThrottlerWithValues
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.redux.global.FiatCurrencyName
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.currenciesRepository
|
||||
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.CardCurrencies
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import com.tangem.tap.features.wallet.redux.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
||||
class TapWalletManager {
|
||||
val walletManagerFactory: WalletManagerFactory
|
||||
by lazy { WalletManagerFactory(blockchainSdkConfig) }
|
||||
|
||||
private val coinMarketCapService = CoinMarketCapService()
|
||||
val rates: RatesRepository = RatesRepository()
|
||||
|
||||
private val blockchainSdkConfig by lazy {
|
||||
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
|
||||
}
|
||||
|
||||
private val walletManagersThrottler = ThrottlerWithValues<Blockchain, Result<Wallet>>(10000)
|
||||
private val fiatRatesThrottler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
|
||||
|
||||
suspend fun loadWalletData(walletManager: WalletManager) {
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
|
|
@ -52,73 +42,37 @@ class TapWalletManager {
|
|||
val result = if (walletManagersThrottler.isStillThrottled(blockchain)) {
|
||||
walletManagersThrottler.geValue(blockchain)!!
|
||||
} else {
|
||||
updateThrottlingForWalletManager(walletManager)
|
||||
val safeUpdateResult = walletManager.safeUpdate()
|
||||
walletManagersThrottler.updateThrottlingTo(blockchain)
|
||||
walletManagersThrottler.setValue(blockchain, safeUpdateResult)
|
||||
safeUpdateResult
|
||||
}
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
checkForRentWarning(walletManager)
|
||||
dispatchOnMain(WalletAction.LoadWallet.Success(result.data, blockchainNetwork))
|
||||
}
|
||||
is Result.Failure -> {
|
||||
when (result.error) {
|
||||
is TapError.WalletManagerUpdate.NoAccountError -> {
|
||||
dispatchOnMain(
|
||||
WalletAction.LoadWallet.NoAccount(
|
||||
walletManager.wallet,
|
||||
blockchainNetwork,
|
||||
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
|
||||
)
|
||||
)
|
||||
is TapError.WalletManager.NoAccountError -> {
|
||||
dispatchOnMain(WalletAction.LoadWallet.NoAccount(
|
||||
walletManager.wallet,
|
||||
blockchainNetwork,
|
||||
(result.error as TapError.WalletManager.NoAccountError).customMessage
|
||||
))
|
||||
}
|
||||
else -> {
|
||||
dispatchOnMain(
|
||||
WalletAction.LoadWallet.Failure(
|
||||
walletManager.wallet,
|
||||
result.error.localizedMessage
|
||||
)
|
||||
)
|
||||
dispatchOnMain(WalletAction.LoadWallet.Failure(
|
||||
walletManager.wallet,
|
||||
result.error.localizedMessage
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateThrottlingForWalletManager(walletManager: WalletManager): Result<Wallet> {
|
||||
val newResult = walletManager.safeUpdate()
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
walletManagersThrottler.updateThrottlingTo(blockchain)
|
||||
walletManagersThrottler.setValue(blockchain, newResult)
|
||||
return newResult
|
||||
}
|
||||
|
||||
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
|
||||
val currencies = wallet.getTokens()
|
||||
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
|
||||
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
|
||||
loadFiatRate(fiatCurrency, currencies)
|
||||
}
|
||||
|
||||
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencies: List<Currency>) {
|
||||
// get and submit previous result of equivalents.
|
||||
val throttledResult = currencies.filter { fiatRatesThrottler.isStillThrottled(it) }.map {
|
||||
Pair(it, fiatRatesThrottler.geValue(it))
|
||||
}
|
||||
if (throttledResult.isNotEmpty()) handleFiatRatesResult(throttledResult)
|
||||
|
||||
val toUpdate = currencies.filter { !fiatRatesThrottler.isStillThrottled(it) }
|
||||
toUpdate.forEach {
|
||||
val result = coinMarketCapService.getRate(it.currencySymbol, fiatCurrency)
|
||||
if (result is Result.Success) {
|
||||
fiatRatesThrottler.updateThrottlingTo(it)
|
||||
fiatRatesThrottler.setValue(it, result)
|
||||
}
|
||||
handleFiatRatesResult(listOf(it to result))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onCardScanned(data: ScanResponse) {
|
||||
walletManagersThrottler.clear()
|
||||
// fiatRatesThrottler.clear()
|
||||
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
|
||||
updateConfigManager(data)
|
||||
|
||||
|
|
@ -169,22 +123,18 @@ class TapWalletManager {
|
|||
if (data.card.isMultiwalletAllowed) {
|
||||
loadMultiWalletData(data, blockchain, primaryWalletManager)
|
||||
} else {
|
||||
dispatchOnMain(
|
||||
WalletAction.MultiWallet.AddBlockchains(
|
||||
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
|
||||
listOf(primaryWalletManager)
|
||||
)
|
||||
)
|
||||
dispatchOnMain(WalletAction.MultiWallet.AddBlockchains(
|
||||
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
|
||||
listOf(primaryWalletManager)
|
||||
))
|
||||
}
|
||||
} else {
|
||||
if (data.card.isMultiwalletAllowed) {
|
||||
loadMultiWalletData(data, blockchain, null)
|
||||
}
|
||||
}
|
||||
dispatchOnMain(
|
||||
WalletAction.LoadWallet(),
|
||||
WalletAction.LoadFiatRate()
|
||||
)
|
||||
dispatchOnMain(WalletAction.LoadWallet())
|
||||
dispatchOnMain(WalletAction.LoadFiatRate())
|
||||
}
|
||||
|
||||
private suspend fun loadMultiWalletData(
|
||||
|
|
@ -193,7 +143,9 @@ class TapWalletManager {
|
|||
primaryWalletManager: WalletManager?
|
||||
) {
|
||||
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
|
||||
val savedCurrencies = currenciesRepository.loadSavedCurrencies(scanResponse.card.cardId, scanResponse.card.derivationStyle)
|
||||
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
|
||||
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
|
||||
)
|
||||
|
||||
if (savedCurrencies.isEmpty()) {
|
||||
if (primaryBlockchain != null && primaryWalletManager != null) {
|
||||
|
|
@ -208,7 +160,6 @@ class TapWalletManager {
|
|||
)
|
||||
|
||||
} else {
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
val blockchainNetworks = listOf(
|
||||
BlockchainNetwork(Blockchain.Bitcoin, scanResponse.card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, scanResponse.card)
|
||||
|
|
@ -224,15 +175,15 @@ class TapWalletManager {
|
|||
WalletAction.MultiWallet.AddBlockchains(blockchainNetworks, walletManagers),
|
||||
)
|
||||
}
|
||||
dispatchOnMain(
|
||||
WalletAction.MultiWallet.FindBlockchainsInUse,
|
||||
WalletAction.MultiWallet.FindTokensInUse,
|
||||
)
|
||||
// dispatchOnMain(
|
||||
// WalletAction.MultiWallet.FindBlockchainsInUse,
|
||||
// WalletAction.MultiWallet.FindTokensInUse,
|
||||
// )
|
||||
} else {
|
||||
val walletManagers = if (
|
||||
primaryTokens.isNotEmpty() &&
|
||||
primaryWalletManager != null &&
|
||||
primaryBlockchain != null
|
||||
primaryBlockchain != null && primaryBlockchain != Blockchain.Unknown
|
||||
) {
|
||||
val blockchainsWithoutPrimary = savedCurrencies.filterNot { it.blockchain == primaryBlockchain }
|
||||
walletManagerFactory.makeWalletManagersForApp(
|
||||
|
|
@ -246,7 +197,9 @@ class TapWalletManager {
|
|||
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
|
||||
)
|
||||
savedCurrencies.map {
|
||||
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
|
||||
if (it.tokens.isNotEmpty()) {
|
||||
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -285,53 +238,6 @@ class TapWalletManager {
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkForRentWarning(walletManager: WalletManager) {
|
||||
val rentProvider = walletManager as? RentProvider ?: return
|
||||
|
||||
when (val result = rentProvider.minimalBalanceForRentExemption()) {
|
||||
is com.tangem.blockchain.extensions.Result.Success -> {
|
||||
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean =
|
||||
balance < rentExempt
|
||||
|
||||
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
|
||||
val outgoingTxs =
|
||||
walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
|
||||
val rentExempt = result.data
|
||||
val show = if (outgoingTxs.isEmpty()) {
|
||||
isNeedToShowWarning(balance, rentExempt)
|
||||
} else {
|
||||
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
|
||||
val rest = balance.minus(outgoingAmount)
|
||||
isNeedToShowWarning(rest, rentExempt)
|
||||
}
|
||||
if (!show) return
|
||||
|
||||
val currency = walletManager.wallet.blockchain.currency
|
||||
dispatchOnMain(
|
||||
WalletAction.SetWalletRent(
|
||||
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
|
||||
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
|
||||
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
|
||||
)
|
||||
)
|
||||
}
|
||||
is com.tangem.blockchain.extensions.Result.Failure -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFiatRatesResult(results: List<Pair<Currency, Result<BigDecimal>?>>) {
|
||||
results.map {
|
||||
when (it.second) {
|
||||
is Result.Success -> {
|
||||
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
|
||||
dispatchOnMain(WalletAction.LoadFiatRate.Success(rate))
|
||||
}
|
||||
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Wallet.getFirstToken(): Token? {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ data class WarningMessage(
|
|||
val messageFormatArg: String? = null
|
||||
) {
|
||||
val blockchainList: List<Blockchain>? by lazy {
|
||||
blockchains?.map { Blockchain.fromId(it.toUpperCase()) }
|
||||
blockchains?.map { Blockchain.fromId(it.uppercase()) }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.extensions.containsAny
|
||||
import com.tangem.tap.common.extensions.removeBy
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -16,6 +18,11 @@ class WarningMessagesManager(
|
|||
private val warningsList: MutableList<WarningMessage> = mutableListOf()
|
||||
|
||||
fun load(onComplete: VoidCallback? = null) {
|
||||
// exclude annoying remote debug warnings
|
||||
if (BuildConfig.DEBUG) {
|
||||
onComplete?.invoke()
|
||||
return
|
||||
}
|
||||
warningLoader.load { remoteList ->
|
||||
warningsList.clear()
|
||||
warningsList.addAll(remoteList)
|
||||
|
|
@ -181,6 +188,31 @@ class WarningMessagesManager(
|
|||
WarningMessage.Origin.Local
|
||||
)
|
||||
|
||||
fun restoreFundsWarning(): WarningMessage = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Temporary,
|
||||
priority = WarningMessage.Priority.Warning,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.alert_title,
|
||||
messageResId = R.string.alert_funds_restoration_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
buttonTextId = R.string.warning_button_learn_more
|
||||
)
|
||||
|
||||
const val REMAINING_SIGNATURES_WARNING = 10
|
||||
private const val RESTORE_FUNDS_GUIDE_URL_RU =
|
||||
"https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet"
|
||||
private const val RESTORE_FUNDS_GUIDE_URL_EN =
|
||||
"https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet"
|
||||
|
||||
fun getRestoreFundsGuideUrl(locale: String): String {
|
||||
return if (locale == Locale("ru").language) {
|
||||
RESTORE_FUNDS_GUIDE_URL_RU
|
||||
} else {
|
||||
RESTORE_FUNDS_GUIDE_URL_EN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,12 +21,15 @@ fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean {
|
|||
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
|
||||
if (!isBuyAllowed) return false
|
||||
|
||||
//TODO: temporary, for the 3.32 release, unlock all buy button
|
||||
return true
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchain = currency.blockchain
|
||||
when {
|
||||
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
|
||||
blockchain == Blockchain.Unknown || blockchain == Blockchain.BSC -> false
|
||||
blockchain == Blockchain.Unknown -> false
|
||||
else -> availableToBuy.contains(currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.tap.domain.statePrinter
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.domain.redux.state.StringStateConverter
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ScanResponseConverter : StringStateConverter<AppState> {
|
||||
private val converter = MoshiJsonConverter.INSTANCE
|
||||
|
||||
override fun convert(stateHolder: AppState): String {
|
||||
val scanResponse = stateHolder.globalState.scanResponse ?: return "NULL"
|
||||
|
||||
val scanResponseMap = mutableMapOf<String, Any?>()
|
||||
val derivedKeysMap = mutableMapOf<String, Any?>()
|
||||
scanResponse.derivedKeys.forEach { (keyWalletPubKey, mapExPubKeys) ->
|
||||
val exPubKeysMap = mapExPubKeys.entries.map { (derivationPath, exPubKey) ->
|
||||
mapOf(
|
||||
"derivationPath" to derivationPath.rawPath,
|
||||
"extendedPublicKey" to mapOf(
|
||||
"publicKey" to exPubKey.publicKey.toHexString(),
|
||||
"chainCode" to exPubKey.chainCode.toHexString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
derivedKeysMap[keyWalletPubKey.bytes.toHexString()] = exPubKeysMap
|
||||
|
||||
}
|
||||
scanResponseMap["derivedKeys"] = derivedKeysMap
|
||||
|
||||
val json = converter.prettyPrint(scanResponseMap)
|
||||
|
||||
return json
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun printScanResponseState() {
|
||||
val stringState = ScanResponseConverter().convert(store.state)
|
||||
Timber.d(stringState)
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.tap.domain.statePrinter
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.domain.redux.state.StringStateConverter
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class WalletStateConverter : StringStateConverter<AppState> {
|
||||
private val converter = MoshiJsonConverter.INSTANCE
|
||||
|
||||
override fun convert(stateHolder: AppState): String {
|
||||
val walletState = stateHolder.walletState
|
||||
|
||||
val walletManagers = mutableListOf<Map<String, Any?>>()
|
||||
walletState.walletManagers.forEach {
|
||||
val wallet = it.wallet
|
||||
|
||||
val walletManagerMap = mutableMapOf<String, Any?>()
|
||||
walletManagerMap["walletManager"] = mapOf<String, Any>(
|
||||
"wallet" to convertWallet(it.wallet),
|
||||
)
|
||||
|
||||
walletManagers.add(walletManagerMap)
|
||||
}
|
||||
val json = converter.prettyPrint(walletManagers)
|
||||
return json
|
||||
}
|
||||
|
||||
private fun convertWallet(wallet: Wallet): MutableMap<String, Any> {
|
||||
val walletMap = mutableMapOf<String, Any>()
|
||||
val amounts = mutableMapOf<String, Any>()
|
||||
wallet.amounts.forEach { (type, amount) ->
|
||||
val amountMap = mapOf<String, Any?>(
|
||||
"value" to amount.value?.toPlainString(),
|
||||
"currencySymbol" to amount.currencySymbol,
|
||||
"decimals" to amount.decimals,
|
||||
"type" to amount.type::class.java.simpleName,
|
||||
)
|
||||
amounts[type::class.java.simpleName] = amountMap
|
||||
}
|
||||
|
||||
val publicKeyMap = mapOf(
|
||||
"seedKey" to wallet.publicKey.seedKey,
|
||||
"derivedKey" to wallet.publicKey.derivedKey,
|
||||
"derivationPath" to wallet.publicKey.derivationPath?.rawPath,
|
||||
"blockchainKey" to wallet.publicKey.blockchainKey,
|
||||
)
|
||||
|
||||
walletMap["address"] = wallet.address
|
||||
walletMap["blockchain"] = wallet.blockchain.name
|
||||
walletMap["curve"] = wallet.blockchain.getSupportedCurves()[0].curve
|
||||
walletMap["publicKey"] = publicKeyMap
|
||||
walletMap["amounts"] = amounts
|
||||
walletMap["addresses"] = wallet.addresses.toString()
|
||||
walletMap["cardId"] = wallet.cardId
|
||||
|
||||
return walletMap
|
||||
}
|
||||
}
|
||||
|
||||
fun printWalletState() {
|
||||
val stringState = WalletStateConverter().convert(store.state)
|
||||
Timber.d(stringState)
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.*
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.TapWorkarounds.isExcluded
|
||||
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
|
|
@ -228,7 +227,7 @@ private class ScanWalletProcessor(
|
|||
|
||||
private fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
|
||||
val currenciesRepository = currenciesRepository ?: return emptyList()
|
||||
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.derivationStyle).toMutableList()
|
||||
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
|
||||
|
||||
val blockchainsToDerive = cardCurrencies.ifEmpty {
|
||||
mutableListOf(
|
||||
|
|
|
|||
|
|
@ -100,19 +100,16 @@ class CurrenciesRepository(val context: Application) {
|
|||
|
||||
fun loadSavedCurrencies(
|
||||
cardId: String,
|
||||
derivationStyle: DerivationStyle? = null
|
||||
isHdWalletSupported: Boolean = false
|
||||
): List<BlockchainNetwork> {
|
||||
if (DemoHelper.isDemoCardId(cardId)) {
|
||||
return loadDemoCurrencies(cardId)
|
||||
}
|
||||
return try {
|
||||
val json = context.readFileText(getFileNameForBlockchains(cardId))
|
||||
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: loadSavedCurrenciesOldWay(
|
||||
cardId,
|
||||
derivationStyle
|
||||
)
|
||||
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,12 +123,27 @@ class CurrenciesRepository(val context: Application) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun tryToLoadPreviousFormatAndMigrate(
|
||||
cardId: String,
|
||||
isHdWalletSupported: Boolean = false
|
||||
): List<BlockchainNetwork> {
|
||||
return try {
|
||||
loadSavedCurrenciesOldWay(
|
||||
cardId,
|
||||
isHdWalletSupported
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSavedCurrenciesOldWay(
|
||||
cardId: String, derivationStyle: DerivationStyle?
|
||||
cardId: String, isHdWalletSupported: Boolean = false
|
||||
): List<BlockchainNetwork> {
|
||||
val blockchains = loadSavedBlockchains(cardId)
|
||||
val tokens = loadSavedTokens(cardId)
|
||||
val currencies = getSupportedTokens()
|
||||
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
|
||||
val blockchainNetworks = blockchains.map { blockchain ->
|
||||
BlockchainNetwork(
|
||||
blockchain = blockchain,
|
||||
|
|
@ -141,7 +153,7 @@ class CurrenciesRepository(val context: Application) {
|
|||
.map {
|
||||
val token = it.toToken()
|
||||
val id = currencies
|
||||
.find { it.contracts?.find { it.address == token.contractAddress } != null }
|
||||
.find { it.contracts.find { it.address == token.contractAddress } != null }
|
||||
?.id
|
||||
token.copy(id = id)
|
||||
}
|
||||
|
|
@ -168,7 +180,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)
|
||||
return currenciesAdapter.fromJson(json)!!.tokens.map { Currency.fromJsonObject(it) }
|
||||
return currenciesAdapter.fromJson(json)!!.coins
|
||||
.map { Currency.fromJsonObject(it) }
|
||||
}
|
||||
|
||||
private fun loadTokensJson(blockchain: Blockchain): String? {
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@ data class CurrencyFromJson(
|
|||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val contracts: List<ContractFromJson>? = null
|
||||
val networks: List<ContractFromJson>? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContractFromJson(
|
||||
val networkId: String,
|
||||
val address: String,
|
||||
val decimalCount: Int
|
||||
val contractAddress: String?,
|
||||
val decimalCount: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CurrenciesFromJson(
|
||||
val imageHost: String,
|
||||
val tokens: List<CurrencyFromJson>
|
||||
val imageHost: String?,
|
||||
val coins: List<CurrencyFromJson>
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ data class Currency(
|
|||
val name: String,
|
||||
val symbol: String,
|
||||
val iconUrl: String,
|
||||
val contracts: List<Contract>?
|
||||
val contracts: List<Contract>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
|
@ -45,7 +45,7 @@ data class Currency(
|
|||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
iconUrl = getIconUrl(currency.id),
|
||||
contracts = currency.contracts?.toContracts()
|
||||
contracts = currency.networks?.toContracts() ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -54,8 +54,8 @@ data class Currency(
|
|||
data class Contract(
|
||||
val networkId: String,
|
||||
val blockchain: Blockchain,
|
||||
val address: String,
|
||||
val decimalCount: Int,
|
||||
val address: String?,
|
||||
val decimalCount: Int?,
|
||||
val iconUrl: String,
|
||||
) {
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ data class Contract(
|
|||
return Contract(
|
||||
networkId = contract.networkId,
|
||||
blockchain = blockchain,
|
||||
address = contract.address,
|
||||
address = contract.contractAddress,
|
||||
decimalCount = contract.decimalCount,
|
||||
iconUrl = getIconUrl(contract.networkId)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class DemoConfig {
|
|||
)
|
||||
|
||||
val demoCardIds: List<String> by lazy {
|
||||
val demoIds = (releaseDemoCardIds + testDemoCardIds).toMutableList()
|
||||
val demoIds = getReleaseIds().toMutableList()
|
||||
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
|
||||
|
||||
return@lazy demoIds.distinct()
|
||||
|
|
@ -103,6 +103,12 @@ class DemoConfig {
|
|||
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
|
||||
?: Amount(BigDecimal.ZERO, blockchain).copy()
|
||||
|
||||
private fun getReleaseIds(): List<String> {
|
||||
return (releaseDemoCardIds +
|
||||
releaseDemoCardIds_19042022 +
|
||||
testDemoCardIds).distinct()
|
||||
}
|
||||
|
||||
private val releaseDemoCardIds = mutableListOf<String>(
|
||||
// Tangem Wallet:
|
||||
"AC01000000041100",
|
||||
|
|
@ -145,14 +151,113 @@ class DemoConfig {
|
|||
"AB02000000051059",
|
||||
)
|
||||
|
||||
// https://tangem.slack.com/archives/GMXC6PP71/p1650360610759269
|
||||
private val releaseDemoCardIds_19042022 = listOf(
|
||||
// Wallet
|
||||
"AC01000000045754",
|
||||
"AC01000000041662",
|
||||
"AC01000000041647",
|
||||
"AC01000000041209",
|
||||
"AC01000000042462",
|
||||
"AC01000000041100",
|
||||
"AC01000000041621",
|
||||
"AC01000000045960",
|
||||
"AC01000000041092",
|
||||
"AC01000000041217",
|
||||
"AC01000000013489",
|
||||
"AC01000000028610",
|
||||
"AC01000000028701",
|
||||
"AC01000000028578",
|
||||
"AC01000000027281",
|
||||
"AC01000000027216",
|
||||
"AC01000000028594",
|
||||
"AC01000000028602",
|
||||
"AC01000000028636",
|
||||
"AC01000000013968",
|
||||
"AC01000000027208",
|
||||
"AC01000000013471",
|
||||
"AC01000000028586",
|
||||
"AC01000000013703",
|
||||
"AC01000000028628",
|
||||
"AC01000000028693",
|
||||
"AC01000000028685",
|
||||
"AC01000000013950",
|
||||
"AC01000000013828",
|
||||
"AC01000000013497",
|
||||
"AC01000000013836",
|
||||
"AC01000000013505",
|
||||
// Note BTC
|
||||
"AB01000000059608",
|
||||
"AB01000000046647",
|
||||
"AB01000000046571",
|
||||
"AB01000000046746",
|
||||
"AB01000000059574",
|
||||
"AB01000000046753",
|
||||
"AB01000000046605",
|
||||
"AB01000000046761",
|
||||
"AB01000000046720",
|
||||
"AB01000000046530",
|
||||
"AB01000000016475",
|
||||
"AB01000000016483",
|
||||
"AB01000000016491",
|
||||
"AB01000000020709",
|
||||
"AB01000000020717",
|
||||
"AB01000000015550",
|
||||
"AB01000000015394",
|
||||
"AB01000000016079",
|
||||
"AB01000000016087",
|
||||
"AB01000000016095",
|
||||
"AB01000000020915",
|
||||
"AB01000000017184",
|
||||
"AB01000000020907",
|
||||
"AB01000000017192",
|
||||
"AB01000000016210",
|
||||
"AB01000000016111",
|
||||
"AB01000000016103",
|
||||
"AB01000000015766",
|
||||
"AB01000000015774",
|
||||
"AB01000000015782",
|
||||
"AB01000000022598",
|
||||
"AB01000000022580",
|
||||
// Note ETH
|
||||
"AB02000000051083",
|
||||
"AB02000000051059",
|
||||
"AB02000000051158",
|
||||
"AB02000000050986",
|
||||
"AB02000000051026",
|
||||
"AB02000000050960",
|
||||
"AB02000000051042",
|
||||
"AB02000000051091",
|
||||
"AB02000000051034",
|
||||
"AB02000000051133",
|
||||
"AB02000000019924",
|
||||
"AB02000000019932",
|
||||
"AB02000000022092",
|
||||
"AB02000000022282",
|
||||
"AB02000000023983",
|
||||
"AB02000000023439",
|
||||
"AB02000000020328",
|
||||
"AB02000000020310",
|
||||
"AB02000000021565",
|
||||
"AB02000000022357",
|
||||
"AB02000000023355",
|
||||
"AB02000000022324",
|
||||
"AB02000000022100",
|
||||
"AB02000000019999",
|
||||
"AB02000000020013",
|
||||
"AB02000000020005",
|
||||
"AB02000000020021",
|
||||
"AB02000000020039",
|
||||
"AB02000000020278",
|
||||
"AB02000000020252",
|
||||
"AB02000000018652",
|
||||
"AB02000000018561",
|
||||
)
|
||||
|
||||
private val testDemoCardIds = listOf(
|
||||
"FB20000000000186", // Note ETH
|
||||
"FB10000000000196", // Note BTC
|
||||
"FB30000000000176", // Wallet
|
||||
//TODO: delete bellow ids before 3.28 release
|
||||
"AB01000000045060", // Note BTC
|
||||
"AB02000000045028", // Note ETH
|
||||
"AC79000000000004", // Wallet 4.46
|
||||
)
|
||||
|
||||
private val debugTestDemoCardIds = listOf<String>(
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ 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.tap.network.coinmarketcap.FiatCurrency
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ sealed class DetailsAction : Action {
|
|||
val cardTou: CardTou,
|
||||
val fiatCurrencyName: FiatCurrencyName,
|
||||
val fiatCurrencies: List<FiatCurrencyName>? = null,
|
||||
val tangemTechService: TangemTechService,
|
||||
) : DetailsAction()
|
||||
|
||||
object ShowDisclaimer : DetailsAction()
|
||||
|
|
@ -46,7 +48,7 @@ sealed class DetailsAction : Action {
|
|||
object CreateBackup : DetailsAction()
|
||||
|
||||
sealed class AppCurrencyAction : DetailsAction() {
|
||||
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
|
||||
data class SetCurrencies(val currencies: List<CurrenciesResponse.Currency>) : AppCurrencyAction()
|
||||
object ChooseAppCurrency : AppCurrencyAction()
|
||||
object Cancel : AppCurrencyAction()
|
||||
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletM
|
|||
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.network.coinmarketcap.CoinMarketCapService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -33,7 +32,7 @@ class DetailsMiddleware {
|
|||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is DetailsAction.PrepareScreen -> prepareData()
|
||||
is DetailsAction.PrepareScreen -> prepareData(action)
|
||||
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
|
||||
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
|
||||
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
|
||||
|
|
@ -68,23 +67,24 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun prepareData() {
|
||||
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 loadedCurrencies = preferencesStorage.getFiatCurrencies()
|
||||
if (loadedCurrencies.isNullOrEmpty()) {
|
||||
val response = CoinMarketCapService().getFiatCurrencies()
|
||||
withContext(Dispatchers.Main) {
|
||||
when (response) {
|
||||
is Result.Success -> {
|
||||
preferencesStorage.saveFiatCurrencies(response.data)
|
||||
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(response.data))
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(loadedCurrencies))
|
||||
}
|
||||
is Result.Failure -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +136,7 @@ class DetailsMiddleware {
|
|||
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())
|
||||
|
|
|
|||
|
|
@ -43,13 +43,15 @@ private fun handlePrepareScreen(
|
|||
action: DetailsAction.PrepareScreen,
|
||||
state: DetailsState,
|
||||
): DetailsState {
|
||||
val backupIsActive = action.scanResponse.card.backupStatus?.isActive ?: false
|
||||
|
||||
return DetailsState(
|
||||
scanResponse = action.scanResponse,
|
||||
wallets = action.wallets,
|
||||
cardInfo = action.scanResponse.card.toCardInfo(),
|
||||
appCurrencyState = AppCurrencyState(action.fiatCurrencyName),
|
||||
appCurrencyState = state.appCurrencyState.copy(
|
||||
fiatCurrencyName = action.fiatCurrencyName,
|
||||
showAppCurrencyDialog = false,
|
||||
),
|
||||
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ 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.network.coinmarketcap.FiatCurrency
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.StateType
|
||||
import java.util.*
|
||||
|
|
@ -51,8 +51,9 @@ data class SecurityScreenState(
|
|||
)
|
||||
|
||||
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
||||
|
||||
data class AppCurrencyState(
|
||||
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
|
||||
val showAppCurrencyDialog: Boolean = false,
|
||||
val fiatCurrencies: List<FiatCurrency>? = null,
|
||||
val fiatCurrencies: List<CurrenciesResponse.Currency>? = null,
|
||||
)
|
||||
|
|
@ -257,7 +257,7 @@ class WalletConnectMiddleware {
|
|||
blockchainToMake,
|
||||
card.derivationStyle?.let { DerivationParams.Default(it) }
|
||||
)
|
||||
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.derivationStyle)
|
||||
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
|
||||
.find { it.blockchain == blockchainToMake } != null
|
||||
) {
|
||||
walletManager?.let {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ 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.network.coinmarketcap.FiatCurrency
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
@ -13,11 +13,11 @@ class CurrencySelectionDialog {
|
|||
|
||||
var dialog: AlertDialog? = null
|
||||
|
||||
fun show(currencies: List<FiatCurrency>, currentAppCurrency: FiatCurrencyName, context: Context) {
|
||||
fun show(currenciesList: List<CurrenciesResponse.Currency>, currentAppCurrency: FiatCurrencyName, context: Context) {
|
||||
|
||||
if (dialog == null) {
|
||||
val currenciesToShow = currencies.map { it.toFormattedString() }.toTypedArray()
|
||||
var currentSelection = currencies.indexOfFirst { it.symbol == currentAppCurrency }
|
||||
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))
|
||||
|
|
@ -25,8 +25,8 @@ class CurrencySelectionDialog {
|
|||
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
|
||||
}
|
||||
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
|
||||
val selectedCurrency = currencies[currentSelection]
|
||||
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.symbol))
|
||||
val selectedCurrency = currenciesList[currentSelection]
|
||||
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.code))
|
||||
}
|
||||
.setOnDismissListener {
|
||||
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ class EmailDataBuilder(
|
|||
appendLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
if (token.id != null) builder.appendKeyValue("Id", token.id ?: "")
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,68 @@
|
|||
package com.tangem.tap.features.home.compose
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalUnitApi::class)
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun AutoSizeText(
|
||||
fun TextAutoSize(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
fontSizeRange: FontSizeRange,
|
||||
) {
|
||||
val scaledTextStyle = remember { mutableStateOf(textStyle) }
|
||||
val fontSizeValue = remember { mutableStateOf(fontSizeRange.max.value) }
|
||||
val readyToDraw = remember { mutableStateOf(false) }
|
||||
|
||||
val onTextLayout: (TextLayoutResult) -> Unit = { result ->
|
||||
if (result.didOverflowWidth) {
|
||||
val fontSize = result.size.width / result.multiParagraph.width
|
||||
scaledTextStyle.value =
|
||||
// scaledTextStyle.value.copy(fontSize = TextUnit(fontSize, TextUnitType.Sp))
|
||||
scaledTextStyle.value.copy(fontSize = scaledTextStyle.value.fontSize * 0.9)
|
||||
} else {
|
||||
readyToDraw.value = true
|
||||
}
|
||||
val textState = remember { mutableStateOf(text) }
|
||||
if (textState.value != text) {
|
||||
readyToDraw.value = false
|
||||
fontSizeValue.value = fontSizeRange.max.value
|
||||
textState.value = text
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
||||
text = text,
|
||||
modifier = modifier.drawWithContent {
|
||||
if (readyToDraw.value) {
|
||||
drawContent()
|
||||
}
|
||||
},
|
||||
softWrap = false,
|
||||
onTextLayout = onTextLayout,
|
||||
style = scaledTextStyle.value,
|
||||
)
|
||||
style = textStyle,
|
||||
fontSize = fontSizeValue.value.sp,
|
||||
onTextLayout = {
|
||||
if (it.hasVisualOverflow) {
|
||||
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
|
||||
if (nextFontSizeValue <= fontSizeRange.min.value) {
|
||||
fontSizeValue.value = fontSizeRange.min.value
|
||||
readyToDraw.value = true
|
||||
} else {
|
||||
fontSizeValue.value = nextFontSizeValue * 0.8f
|
||||
}
|
||||
} else {
|
||||
readyToDraw.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
data class FontSizeRange(
|
||||
val min: TextUnit,
|
||||
val max: TextUnit,
|
||||
val step: TextUnit = DEFAULT_TEXT_STEP,
|
||||
) {
|
||||
init {
|
||||
require(min < max) { "min should be less than max, $this" }
|
||||
require(step.value > 0) { "step should be greater than 0, $this" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_TEXT_STEP = 1.sp
|
||||
}
|
||||
}
|
||||
|
|
@ -96,25 +96,33 @@ fun FirstStoriesContent(
|
|||
)
|
||||
|
||||
if (screenState.value != StartingScreenState.MEET_TANGEM) {
|
||||
AutoSizeText(text = text?.let { stringResource(text) } ?: "", textStyle = style,
|
||||
TextAutoSize(
|
||||
modifier = Modifier
|
||||
.padding(start = 40.dp, end = 40.dp)
|
||||
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),)
|
||||
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp)
|
||||
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
|
||||
text = text?.let { stringResource(text) } ?: "",
|
||||
textStyle = style,
|
||||
fontSizeRange = FontSizeRange(20.sp, 60.sp)
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = screenState.value == StartingScreenState.MEET_TANGEM,
|
||||
enter = slideInVertically() { it / 2 }
|
||||
) {
|
||||
AutoSizeText(text = text?.let { stringResource(text) } ?: "", textStyle = style,
|
||||
TextAutoSize(
|
||||
modifier = Modifier
|
||||
.padding(start = 40.dp, end = 40.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 40.dp, end = 40.dp),
|
||||
text = text?.let { stringResource(text) } ?: "",
|
||||
textStyle = style,
|
||||
fontSizeRange = FontSizeRange(20.sp, 60.sp)
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = screenState.value == StartingScreenState.SHOW_CARD ||
|
||||
screenState.value == StartingScreenState.MEET_TANGEM,
|
||||
screenState.value == StartingScreenState.MEET_TANGEM,
|
||||
enter = scaleIn(initialScale = 3f)
|
||||
) {
|
||||
Image(
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class OnboardingManager(
|
|||
|
||||
suspend fun loadArtworkUrl(): String {
|
||||
val cardInfo = cardInfo
|
||||
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
|
||||
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
|
||||
this.cardInfo = cardInfo
|
||||
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo)
|
||||
}
|
||||
|
|
@ -57,11 +57,11 @@ class OnboardingManager(
|
|||
is Result.Failure -> {
|
||||
val error = (result.error as? TapError) ?: TapError.UnknownError
|
||||
when (error) {
|
||||
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
|
||||
// NoInternetConnection, WalletManagerUpdate.InternalError
|
||||
is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error)
|
||||
// NoInternetConnection, WalletManager.InternalError
|
||||
else -> {
|
||||
Timber.e(error.localizedMessage)
|
||||
OnboardingWalletBalance.criticalError(TapError.WalletManagerUpdate.BlockchainIsUnreachableTryLater)
|
||||
OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ data class OnboardingWalletBalance(
|
|||
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
|
||||
|
||||
val amountToCreateAccount: String?
|
||||
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
|
||||
get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null
|
||||
|
||||
companion object {
|
||||
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ sealed class OnboardingWalletAction : Action {
|
|||
sealed class BackupAction : Action {
|
||||
|
||||
object DetermineBackupStep : BackupAction()
|
||||
object IntroduceBackup : BackupAction()
|
||||
object StartBackup : BackupAction()
|
||||
object DismissBackup : BackupAction()
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,15 @@ private fun handleWalletAction(action: Action) {
|
|||
val newAction = when (val backupState = backupService.currentState) {
|
||||
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
|
||||
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
|
||||
else -> null
|
||||
else -> {
|
||||
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
|
||||
walletState.backupState.backupStep == BackupStep.Finished
|
||||
) {
|
||||
BackupAction.IntroduceBackup
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
newAction?.let { store.dispatch(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ class BackupReducer {
|
|||
): BackupState {
|
||||
|
||||
return when (action) {
|
||||
is BackupAction.IntroduceBackup -> BackupState(
|
||||
backupStep = BackupStep.InitBackup,
|
||||
canSkipBackup = state.canSkipBackup,
|
||||
)
|
||||
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
|
||||
BackupAction.StartAddingBackupCards -> {
|
||||
state.copy(backupStep = BackupStep.AddBackupCards)
|
||||
|
|
@ -104,6 +108,7 @@ class BackupReducer {
|
|||
BackupAction.OnAccessCodeDialogClosed -> {
|
||||
state.copy(backupStep = BackupStep.AddBackupCards)
|
||||
}
|
||||
BackupAction.DiscardBackup -> BackupState()
|
||||
|
||||
BackupAction.WritePrimaryCard -> state
|
||||
is BackupAction.WriteBackupCard -> state
|
||||
|
|
@ -117,7 +122,6 @@ class BackupReducer {
|
|||
is BackupAction.CheckAccessCode -> state
|
||||
BackupAction.DetermineBackupStep -> state
|
||||
BackupAction.CheckForUnfinishedBackup -> state
|
||||
BackupAction.DiscardBackup -> state
|
||||
BackupAction.ResumeBackup -> state
|
||||
BackupAction.DiscardSavedBackup -> state
|
||||
BackupAction.StartBackup -> state
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.google.accompanist.appcompattheme.AppCompatTheme
|
|||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
|
@ -57,6 +58,6 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment)
|
|||
|
||||
}
|
||||
}
|
||||
// addBackPressHandler(this)
|
||||
addBackPressHandler(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.domain.ErrorConverter
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DomainErrorConverter(
|
||||
private val context: Context
|
||||
) : ErrorConverter<String> {
|
||||
|
||||
override fun convertError(error: DomainError): String {
|
||||
val errorMessage = when (error) {
|
||||
is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error)
|
||||
else -> null
|
||||
}
|
||||
return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}"
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenConverter(
|
||||
private val context: Context
|
||||
) : ErrorConverter<String> {
|
||||
|
||||
override fun convertError(error: DomainError): String {
|
||||
val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException()
|
||||
|
||||
val rawMessage = when (customTokenError) {
|
||||
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
|
||||
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
|
||||
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
|
||||
AddCustomTokenError.InvalidDecimalsCount -> R.string.custom_token_creation_error_wrong_decimals
|
||||
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields
|
||||
else -> null
|
||||
}
|
||||
return when (rawMessage) {
|
||||
is Int -> context.getString(rawMessage)
|
||||
// is String -> rawMessage
|
||||
else -> "Unknown error: ${customTokenError::class.java.simpleName}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,21 +12,21 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.domain.ErrorConverter
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
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.features.tokens.addCustomToken.DomainErrorConverter
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestAddCustomTokenActions
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
|
|
@ -52,7 +52,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
|
|||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = 90.dp)
|
||||
) {
|
||||
// item { AddCustomTokenDebugActions() }
|
||||
item { TestAddCustomTokenActions() }
|
||||
item {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
|
|
@ -81,7 +81,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
|
|||
@Composable
|
||||
private fun FormFields(state: MutableState<AddCustomTokenState>) {
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { DomainErrorConverter(context) }
|
||||
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
val stateValue = state.value
|
||||
stateValue.form.fieldList.forEach { field ->
|
||||
|
|
@ -98,32 +98,24 @@ private fun FormFields(state: MutableState<AddCustomTokenState>) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun Warnings(warnings: List<AddCustomTokenWarning>) {
|
||||
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
|
||||
if (warnings.isEmpty()) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val warningConverter = remember { DomainErrorConverter(context) }
|
||||
val warningConverter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
Column {
|
||||
warnings.forEachIndexed { index, item ->
|
||||
val modifier = when (index) {
|
||||
0 -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)
|
||||
0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)
|
||||
warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp)
|
||||
else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp)
|
||||
}
|
||||
Surface(
|
||||
AddCustomTokenWarning(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = colorResource(id = R.color.warning_warning),
|
||||
elevation = 4.dp,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = warningConverter.convertError(item),
|
||||
color = colorResource(id = R.color.lightGray0),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
warning = item,
|
||||
converter = warningConverter
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,49 +130,53 @@ private fun AddButton(state: MutableState<AddCustomTokenState>) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun AddCustomTokenFab(
|
||||
private fun AddCustomTokenFab(
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val contentColor = if (isEnabled) {
|
||||
Color.White
|
||||
val contentColor = Color.White
|
||||
val backgroundColor = if (isEnabled) {
|
||||
Color(0xFF1ACE80)
|
||||
} else {
|
||||
colorResource(id = R.color.darkGray1)
|
||||
Color(0xFFB9E6D3)
|
||||
}
|
||||
val elevation = if (!isEnabled) {
|
||||
FloatingActionButtonDefaults.elevation(0.dp, 0.dp)
|
||||
} else {
|
||||
FloatingActionButtonDefaults.elevation()
|
||||
}
|
||||
val backgroundColor = Color(0xFF1ACE80)
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
tint = contentColor,
|
||||
contentDescription = "Add",
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_add),
|
||||
)
|
||||
},
|
||||
onClick = onClick,
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
)
|
||||
ToggledRippleTheme(isEnabled) {
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
tint = contentColor,
|
||||
contentDescription = "Add",
|
||||
)
|
||||
},
|
||||
text = { Text(text = stringResource(id = R.string.common_add)) },
|
||||
onClick = { if (isEnabled) onClick() },
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
elevation = elevation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ScreenFieldData(
|
||||
val field: DataField<*>,
|
||||
val error: AddCustomTokenError?,
|
||||
val errorConverter: ErrorConverter<String>,
|
||||
val errorConverter: ModuleMessageConverter,
|
||||
val viewState: ViewStates.TokenField
|
||||
) {
|
||||
companion object {
|
||||
fun fromState(
|
||||
field: DataField<*>,
|
||||
state: AddCustomTokenState,
|
||||
errorConverter: DomainErrorConverter
|
||||
errorConverter: ModuleMessageConverter
|
||||
): ScreenFieldData {
|
||||
return ScreenFieldData(
|
||||
field = field,
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ package com.tangem.tap.features.tokens.addCustomToken.compose
|
|||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
|
||||
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
|
||||
import com.tangem.domain.features.addCustomToken.TokenField
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.BlockchainSpinner
|
||||
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
|
||||
import com.tangem.tap.common.compose.SpacerH8
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.tap.common.compose.*
|
||||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
|
|
@ -27,19 +27,45 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_contract_address_input_title,
|
||||
placeholder = "0x0000000000000000",
|
||||
placeholder = "0x0000000000000000000000000000000000000000",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
isLoading = screenFieldData.viewState.isLoading,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) }
|
||||
) {
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it)))
|
||||
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasteClearButton(
|
||||
showFirst: Boolean,
|
||||
) {
|
||||
if (showFirst) {
|
||||
val context = LocalContext.current
|
||||
PasteButton(
|
||||
onClick = {
|
||||
context.getFromClipboard()?.let {
|
||||
val fieldData = Field.Data(it.toString(), false)
|
||||
domainStore.dispatch(OnTokenContractAddressChanged(fieldData))
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
} else {
|
||||
ClearButton(
|
||||
onClick = {
|
||||
val fieldData = Field.Data("", false)
|
||||
domainStore.dispatch(OnTokenContractAddressChanged(fieldData))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenNameView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
|
@ -47,14 +73,14 @@ fun TokenNameView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_name_input_title,
|
||||
placeholderId = R.string.custom_token_name_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it)))
|
||||
domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -71,8 +97,8 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat
|
|||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) }
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
|
|
@ -83,13 +109,13 @@ fun TokenSymbolView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_token_symbol_input_title,
|
||||
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
|
|
@ -100,14 +126,14 @@ fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_decimals_input_title,
|
||||
placeholder = "8",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +149,18 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo
|
|||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
dropdownItemView = { blockchain ->
|
||||
val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected)
|
||||
val blockchainName = state.convertBlockchainName(blockchain, notSelected)
|
||||
TitleSubtitle(derivationPathLabel, blockchainName)
|
||||
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
|
||||
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
|
||||
TitleSubtitle(derivationPathName, blockchainName)
|
||||
}
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TestTokenContractAddressView() {
|
||||
|
||||
}
|
||||
|
|
@ -17,5 +17,5 @@ fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRe
|
|||
items = dialog.items,
|
||||
onSelect = dialog.onSelect,
|
||||
onDismissRequest = onDismissRequest
|
||||
) { contract -> TitleSubtitle(dialog.networkIdConverter(contract.networkId), contract.address) }
|
||||
) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") }
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken.compose
|
||||
package com.tangem.tap.features.tokens.addCustomToken.compose.test
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Button
|
||||
|
|
@ -12,21 +12,17 @@ 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.common.services.Result
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.TangemTechServiceManager
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun AddCustomTokenDebugActions() {
|
||||
if (!BuildConfig.DEBUG) return
|
||||
fun TestAddCustomTokenActions() {
|
||||
if (!BuildConfig.TEST_ACTION_ENABLED) return
|
||||
|
||||
Column() {
|
||||
// deep test
|
||||
|
|
@ -40,27 +36,22 @@ fun AddCustomTokenDebugActions() {
|
|||
ActionRow("All in one") { AllInOne() }
|
||||
|
||||
// Any action
|
||||
// ActionRow("CustomActions - find tokens active=false, decimals != null") { CustomActions() }
|
||||
// ActionRow("CustomActions - find coins active=false, decimals != null") { CustomActions() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AllInOne() {
|
||||
// validation error
|
||||
ContractAddressButton(
|
||||
name = "invalid",
|
||||
address = "unk"
|
||||
)
|
||||
// ContractAddressButton(
|
||||
// name = "invalid",
|
||||
// address = "unk"
|
||||
// )
|
||||
// active = true
|
||||
ContractAddressButton(
|
||||
name = "true",
|
||||
address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
||||
)
|
||||
// active = false, decimalCount != null
|
||||
ContractAddressButton(
|
||||
name = "false",
|
||||
address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f"
|
||||
)
|
||||
// more than one network
|
||||
ContractAddressButton(
|
||||
name = ">1 network",
|
||||
|
|
@ -68,9 +59,17 @@ private fun AllInOne() {
|
|||
)
|
||||
// unknown
|
||||
ContractAddressButton(
|
||||
name = "unk",
|
||||
name = "unknown",
|
||||
address = "0x1111111111111111112111111111111111111113"
|
||||
)
|
||||
ContractAddressButton(
|
||||
name = "full custom",
|
||||
address = "0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1",
|
||||
tokenNetwork = Blockchain.Ethereum,
|
||||
tokenName = "Test unknown",
|
||||
tokenSymbol = "TU",
|
||||
tokenDecimals = "5",
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -143,41 +142,6 @@ private fun UnknownContracts() {
|
|||
|
||||
@Composable
|
||||
private fun CustomActions() {
|
||||
|
||||
CustomActionButton(
|
||||
name = "Find tokens in several networks",
|
||||
action = {
|
||||
val manager = TangemTechServiceManager(TangemTechService())
|
||||
val currencies = manager.tokens()
|
||||
val asdfsd = mutableMapOf<String, MutableList<Any>>()
|
||||
val contractAddresses = currencies.mapNotNull { currency ->
|
||||
currency.contracts?.map { it.address }
|
||||
}.flatten()
|
||||
contractAddresses.take(500).forEachIndexed() { index, address ->
|
||||
when (val result = manager.checkAddress(address)) {
|
||||
is Result.Success -> {
|
||||
val contractList = mutableListOf<Any>()
|
||||
result.data.forEach { token ->
|
||||
token.contracts.forEach { contract ->
|
||||
if (!contract.active && contract.decimalCount != null) {
|
||||
contractList.add(contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contractList.isNotEmpty()) {
|
||||
val list = asdfsd[address] ?: mutableListOf()
|
||||
list.addAll(contractList)
|
||||
asdfsd[address] = list
|
||||
}
|
||||
Timber.e("Success. handle $index item from size ${contractAddresses.size}. Result = ${asdfsd.size}")
|
||||
}
|
||||
is Result.Failure -> {}
|
||||
}
|
||||
}
|
||||
val result = asdfsd.filter { it.value.size > 1 }
|
||||
if (result.isEmpty()) return@CustomActionButton
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -214,10 +178,26 @@ private fun ActionButton(
|
|||
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(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false)))
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +218,7 @@ private fun CustomActionButton(
|
|||
}
|
||||
|
||||
private fun resetTokenValues() {
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
|
||||
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)))
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.features.tokens.redux
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationParams
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
|
|
@ -13,13 +13,12 @@ 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.features.addCustomToken.CompleteData
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
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.global.GlobalAction
|
||||
|
|
@ -28,19 +27,16 @@ 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.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.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class TokensMiddleware {
|
||||
|
||||
val tokensMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
val tokensMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
|
|
@ -58,8 +54,10 @@ class TokensMiddleware {
|
|||
val isTestcard = scanResponse?.card?.isTestCard ?: false
|
||||
|
||||
scope.launch {
|
||||
val currencies = async { currenciesRepository.getSupportedTokens(isTestcard)
|
||||
.filter(action.supportedBlockchains?.toSet()) }
|
||||
val currencies = async {
|
||||
currenciesRepository.getSupportedTokens(isTestcard)
|
||||
.filter(action.supportedBlockchains?.toSet())
|
||||
}
|
||||
val delay = async { delay(600) }
|
||||
delay.await()
|
||||
store.dispatchOnMain(TokensAction.LoadCurrencies.Success(currencies.await()))
|
||||
|
|
@ -69,58 +67,80 @@ class TokensMiddleware {
|
|||
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
val currentTokens = store.state.tokensState.addedWallets.toTokens()
|
||||
val currentBlockchains = store.state.tokensState.addedWallets.toBlockchains(
|
||||
store.state.tokensState.derivationStyle
|
||||
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
|
||||
scanResponse.card.derivationStyle
|
||||
)
|
||||
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
|
||||
scanResponse.card.derivationStyle
|
||||
)
|
||||
|
||||
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
|
||||
val blockchainsToRemove = currentBlockchains.filter { !action.addedBlockchains.contains(it) }
|
||||
|
||||
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it.token) }
|
||||
val tokensToRemove = currentTokens.filter { token -> !action.addedTokens.any { it.token == token } }
|
||||
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
|
||||
val tokensToRemove = currentTokens.filter {
|
||||
token -> !action.addedTokens.any { it.token == token.token }
|
||||
}
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
|
||||
removeCurrenciesIfNeeded(blockchainsToRemove, tokensToRemove)
|
||||
removeCurrenciesIfNeeded(convertToCurrencies(
|
||||
blockchains = blockchainsToRemove,
|
||||
tokens = tokensToRemove,
|
||||
derivationStyle = derivationStyle
|
||||
))
|
||||
|
||||
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
|
||||
store.dispatchDebugErrorNotification("Nothing to save")
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
return
|
||||
}
|
||||
|
||||
val currencyList = convertToCurrencies(
|
||||
blockchains = blockchainsToAdd,
|
||||
tokens = tokensToAdd,
|
||||
derivationStyle = derivationStyle
|
||||
)
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingBlockchains(scanResponse, blockchainsToAdd, tokensToAdd)
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAdd(it, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
} else {
|
||||
submitAdd(blockchainsToAdd, tokensToAdd, scanResponse)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
submitAdd(scanResponse, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
|
||||
val tokensState = store.state.tokensState
|
||||
val addedTokensList = tokensState.addedTokens.map {
|
||||
DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain)
|
||||
private fun convertToCurrencies(
|
||||
blockchains: List<Blockchain>,
|
||||
tokens: List<TokenWithBlockchain>,
|
||||
derivationStyle: DerivationStyle?
|
||||
): List<Currency> {
|
||||
return blockchains.map {
|
||||
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
|
||||
} + tokens.map {
|
||||
Currency.Token(
|
||||
it.token,
|
||||
it.blockchain,
|
||||
it.blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
)
|
||||
}
|
||||
val addedBlockchains = tokensState.addedBlockchains.map { it }
|
||||
val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains)
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
||||
|
||||
val callback = fun(data: CompleteData) {
|
||||
Timber.e("Yoooohhhoooo")
|
||||
}
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
}
|
||||
|
||||
private fun deriveMissingBlockchains(
|
||||
scanResponse: ScanResponse,
|
||||
blockchains: List<Blockchain>,
|
||||
tokens: List<TokenWithBlockchain>
|
||||
currencyList: List<Currency>,
|
||||
onSuccess: (ScanResponse) -> Unit,
|
||||
) {
|
||||
val derivationDataList = listOfNotNull(
|
||||
getDerivations(EllipticCurve.Secp256k1, scanResponse, blockchains, tokens),
|
||||
getDerivations(EllipticCurve.Ed25519, scanResponse, blockchains, tokens)
|
||||
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
|
||||
getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList)
|
||||
)
|
||||
val derivations = derivationDataList.map { it.derivations }.toMap()
|
||||
val derivations = derivationDataList.associate { it.derivations }
|
||||
if (derivations.isEmpty()) {
|
||||
onSuccess(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.derivePublicKeys(
|
||||
|
|
@ -145,13 +165,11 @@ class TokensMiddleware {
|
|||
derivedKeys = updatedDerivedKeys
|
||||
)
|
||||
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
|
||||
submitAdd(blockchains, tokens, updatedScanResponse)
|
||||
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
onSuccess(updatedScanResponse)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
store.dispatchErrorNotification(TapError.CustomError("Error adding tokens"))
|
||||
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,20 +178,31 @@ class TokensMiddleware {
|
|||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
blockchains: List<Blockchain>,
|
||||
tokens: List<TokenWithBlockchain>
|
||||
currencyList: List<Currency>,
|
||||
): DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
val derivationPathsCandidates = (blockchains + tokens.map { it.blockchain }).distinct()
|
||||
.mapNotNull { it.derivationPath(scanResponse.card.derivationStyle) }
|
||||
val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter {
|
||||
it.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull {
|
||||
it.derivationPath(scanResponse.card.derivationStyle)
|
||||
}
|
||||
|
||||
val customTokensCandidates = currencyList.filter {
|
||||
it.blockchain.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
|
||||
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
return DerivationData(
|
||||
derivations = mapKeyOfWalletPublicKey to toDerive,
|
||||
alreadyDerivedKeys = alreadyDerivedKeys,
|
||||
|
|
@ -188,37 +217,93 @@ class TokensMiddleware {
|
|||
)
|
||||
|
||||
private fun submitAdd(
|
||||
blockchains: List<Blockchain>, tokens: List<TokenWithBlockchain>, scanResponse: ScanResponse,
|
||||
scanResponse: ScanResponse,
|
||||
currencyList: List<Currency>,
|
||||
) {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
|
||||
(blockchains.mapNotNull {
|
||||
val walletManager = factory.makeWalletManagerForApp(
|
||||
scanResponse, it,
|
||||
scanResponse.card.derivationStyle?.let { DerivationParams.Default(it) }
|
||||
) ?: return@mapNotNull null
|
||||
WalletAction.MultiWallet.AddBlockchain(BlockchainNetwork.fromWalletManager(walletManager), walletManager)
|
||||
} + tokens.map {
|
||||
val blockchainNetwork = BlockchainNetwork(it.blockchain, scanResponse.card)
|
||||
WalletAction.MultiWallet.AddToken(it.token, blockchainNetwork)
|
||||
}).forEach { store.dispatchOnMain(it) }
|
||||
}
|
||||
val addActions = currencyList.mapNotNull { currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
|
||||
|
||||
private fun removeCurrenciesIfNeeded(blockchains: List<Blockchain>, tokens: List<Token>) {
|
||||
if (tokens.isNotEmpty()) {
|
||||
tokens.forEach { token ->
|
||||
store.state.walletState.getWalletData(token)?.let {
|
||||
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
|
||||
val derivationParams = derivationStyle?.let {
|
||||
when (derivationPath) {
|
||||
null -> DerivationParams.Default(derivationStyle)
|
||||
else -> DerivationParams.Custom(derivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
val walletManager = factory.makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = currency.blockchain,
|
||||
derivationParams = derivationParams
|
||||
) ?: return@mapNotNull null
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, walletManager)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val rawDerivationPath = currency.derivationPath
|
||||
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
|
||||
val blockchainNetwork = BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
|
||||
WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blockchains.isNotEmpty()) {
|
||||
blockchains.forEach { blockchain ->
|
||||
store.state.walletState.getWalletData(blockchain)?.let {
|
||||
addActions.forEach { store.dispatchOnMain(it) }
|
||||
}
|
||||
|
||||
private fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
|
||||
if (currencies.isNotEmpty()) {
|
||||
currencies.forEach { currency ->
|
||||
store.state.walletState.getWalletData(currency)?.let {
|
||||
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
|
||||
return currency.derivationPath?.let {
|
||||
!scanResponse.hasDerivation(currency.blockchain, it)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
|
||||
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List<Currency>) {
|
||||
submitAdd(scanResponse, currencyList)
|
||||
// pop from the AddCustomTokenScreen
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
|
||||
val currencyList = listOf(currency)
|
||||
if (isNeedToDerive) {
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAndPopBack(it, currencyList)
|
||||
}
|
||||
} else {
|
||||
submitAndPopBack(scanResponse, currencyList)
|
||||
}
|
||||
}
|
||||
|
||||
val addedCurrencies = store.state.walletState.wallets.map { walletStore ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
}
|
||||
}
|
||||
|
|
@ -22,15 +22,15 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
|
|||
is TokensAction.SetAddedCurrencies -> {
|
||||
|
||||
tokensState.copy(
|
||||
addedBlockchains = action.wallets.toBlockchains(action.derivationStyle),
|
||||
addedTokens = action.wallets.toTokensWithBlockchains(action.derivationStyle),
|
||||
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
|
||||
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
|
||||
addedWallets = action.wallets,
|
||||
derivationStyle = action.derivationStyle
|
||||
)
|
||||
}
|
||||
is TokensAction.SetNonRemovableCurrencies -> {
|
||||
tokensState.copy(
|
||||
nonRemovableBlockchains = action.wallets.toBlockchains(tokensState.derivationStyle),
|
||||
nonRemovableBlockchains = action.wallets.toNonCustomBlockchains(tokensState.derivationStyle),
|
||||
nonRemovableTokens = action.wallets.toTokensContractAddresses(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,13 @@ fun List<WalletData>.toTokensContractAddresses(): List<ContractAddress> {
|
|||
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token?.contractAddress }.distinct()
|
||||
}
|
||||
|
||||
fun List<WalletData>.toTokens(): List<Token> {
|
||||
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }.distinct()
|
||||
fun List<WalletData>.toNonCustomTokens(derivationStyle: DerivationStyle?): List<Token> {
|
||||
return filter { !it.currency.isCustomCurrency(derivationStyle) }
|
||||
.mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }
|
||||
.distinct()
|
||||
}
|
||||
|
||||
fun List<WalletData>.toTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
|
||||
fun List<WalletData>.toNonCustomTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
|
||||
return mapNotNull {
|
||||
if (it.currency !is com.tangem.tap.features.wallet.redux.Currency.Token) return@mapNotNull null
|
||||
if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
||||
|
|
@ -39,7 +41,7 @@ fun List<WalletData>.toTokensWithBlockchains(derivationStyle: DerivationStyle?):
|
|||
}.distinct()
|
||||
}
|
||||
|
||||
fun List<WalletData>.toBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
||||
fun List<WalletData>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
||||
return mapNotNull {
|
||||
if (it.currency.isCustomCurrency(derivationStyle)) {
|
||||
null
|
||||
|
|
@ -58,8 +60,9 @@ fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency
|
|||
if (supportedBlockchains == null) return this
|
||||
return map {
|
||||
it.copy(contracts =
|
||||
it.contracts?.filter {
|
||||
supportedBlockchains.contains(it.blockchain) && it.blockchain.canHandleTokens()
|
||||
it.contracts.filter {
|
||||
supportedBlockchains.contains(it.blockchain) &&
|
||||
(it.blockchain.canHandleTokens() || it.address == null)
|
||||
}
|
||||
)
|
||||
}.filterNot {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
|||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.popular_tokens, menu)
|
||||
|
||||
val addCustomTokenAllowed = store.state.tokensState.allowToAdd
|
||||
menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed
|
||||
|
||||
val menuItem = menu.findItem(R.id.menu_search)
|
||||
val searchView: SearchView = menuItem.actionView as SearchView
|
||||
searchView.queryHint = searchView.getString(R.string.add_token_search_hint)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.tokens.ui.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -10,11 +11,13 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
||||
|
|
@ -36,10 +39,12 @@ fun CollapsedCurrencyItem(
|
|||
.clickable(onClick = { onCurrencyClick(currency.id) })
|
||||
) {
|
||||
val blockchain = Blockchain.fromNetworkId(currency.id)
|
||||
val iconRes = currency.iconUrl
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
model = iconRes,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(currency.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = currency.id,
|
||||
loading = { CurrencyPlaceholderIcon(currency.id) },
|
||||
error = { CurrencyPlaceholderIcon(currency.id) },
|
||||
|
|
@ -55,7 +60,7 @@ fun CollapsedCurrencyItem(
|
|||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = currency.name,
|
||||
text = currency.fullName,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1C1C1E),
|
||||
|
|
@ -64,9 +69,10 @@ fun CollapsedCurrencyItem(
|
|||
Row {
|
||||
if (!currency.contracts.isNullOrEmpty()) {
|
||||
currency.contracts.map { contract ->
|
||||
if (contract.address == currency.symbol) {
|
||||
if (contract.address == null) {
|
||||
BlockchainNetworkItem(
|
||||
blockchain = Blockchain.fromNetworkId(contract.networkId),
|
||||
blockchain = contract.blockchain,
|
||||
isMainNetwork = true,
|
||||
addedBlockchains = addedBlockchains
|
||||
)
|
||||
} else {
|
||||
|
|
@ -91,6 +97,7 @@ fun CollapsedCurrencyItem(
|
|||
} else {
|
||||
BlockchainNetworkItem(
|
||||
blockchain = blockchain,
|
||||
isMainNetwork = false,
|
||||
addedBlockchains = addedBlockchains
|
||||
)
|
||||
}
|
||||
|
|
@ -110,18 +117,39 @@ fun CollapsedCurrencyItem(
|
|||
@Composable
|
||||
fun BlockchainNetworkItem(
|
||||
blockchain: Blockchain?,
|
||||
isMainNetwork: Boolean,
|
||||
addedBlockchains: List<Blockchain>
|
||||
) {
|
||||
val added = addedBlockchains.contains(blockchain)
|
||||
val icon =
|
||||
if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes()
|
||||
if (icon != null) {
|
||||
Image(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
Modifier
|
||||
.size(20.dp)
|
||||
)
|
||||
Box(Modifier
|
||||
.size(20.dp)) {
|
||||
Image(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
Modifier
|
||||
.size(20.dp)
|
||||
)
|
||||
if (isMainNetwork) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(7.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF1ACE80))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(5.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -126,7 +127,7 @@ fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
|
|||
)
|
||||
},
|
||||
onClick = onSaveChanges,
|
||||
backgroundColor = Color(0xFF1ACE80),
|
||||
backgroundColor = colorResource(id = R.color.accent),
|
||||
contentColor = Color.White,
|
||||
modifier = Modifier.padding(bottom = padding.dp)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
||||
|
|
@ -36,9 +37,6 @@ fun ExpandedCurrencyItem(
|
|||
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClicked: (ContractAddress) -> Unit
|
||||
) {
|
||||
val blockchain = Blockchain.fromNetworkId(currency.id)
|
||||
val iconRes = currency.iconUrl
|
||||
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -46,7 +44,10 @@ fun ExpandedCurrencyItem(
|
|||
.clickable(onClick = { onCurrencyClick(currency.id) })
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = iconRes,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(currency.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = currency.id,
|
||||
loading = { CurrencyPlaceholderIcon(currency.id) },
|
||||
error = { CurrencyPlaceholderIcon(currency.id) },
|
||||
|
|
@ -63,7 +64,7 @@ fun ExpandedCurrencyItem(
|
|||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = currency.name,
|
||||
text = currency.fullName,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1C1C1E),
|
||||
|
|
@ -85,9 +86,7 @@ fun ExpandedCurrencyItem(
|
|||
)
|
||||
}
|
||||
|
||||
val blockchains = currency.contracts?.map { it.blockchain } ?: listOfNotNull(
|
||||
Blockchain.fromNetworkId(currency.id)
|
||||
)
|
||||
val blockchains = currency.contracts.map { it.blockchain }
|
||||
|
||||
Row {
|
||||
Box(
|
||||
|
|
@ -132,13 +131,14 @@ fun ExpandedCurrencyItem(
|
|||
.padding(top = 6.dp),
|
||||
) {
|
||||
blockchains.map { blockchain ->
|
||||
val contract = currency.contracts?.firstOrNull { it.blockchain == blockchain }
|
||||
val added = if (contract != null && contract.address != currency.symbol) {
|
||||
val contract = currency.contracts.firstOrNull { it.blockchain == blockchain }
|
||||
?: return@map
|
||||
val added = if (contract.address != null) {
|
||||
addedTokens.map { it.token.contractAddress }.contains(contract.address)
|
||||
} else {
|
||||
addedBlockchains.contains(blockchain)
|
||||
}
|
||||
val canBeRemoved = if (contract != null && contract.address != currency.symbol) {
|
||||
val canBeRemoved = if (contract.address != null) {
|
||||
!nonRemovableTokens.contains(contract.address)
|
||||
} else {
|
||||
!nonRemovableBlockchains.contains(blockchain)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
|
@ -7,6 +8,7 @@ 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.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.redux.ContractAddress
|
||||
|
|
@ -39,7 +41,9 @@ fun ListOfCurrencies(
|
|||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 90.dp)
|
||||
) {
|
||||
|
||||
val filteredCurrencies = if (searchInput.isBlank()) {
|
||||
|
|
@ -66,4 +70,7 @@ fun ListOfCurrencies(
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Currency.fullName: String
|
||||
get() = "${this.name} (${this.symbol})"
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.tokens.ui.compose
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -14,12 +15,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
|
||||
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
||||
import com.tangem.tap.common.extensions.getNetworkName
|
||||
import com.tangem.tap.common.extensions.getRoundIconRes
|
||||
|
|
@ -31,39 +35,57 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NetworkItem(
|
||||
currency: Currency, contract: Contract?,
|
||||
currency: Currency, contract: Contract,
|
||||
blockchain: Blockchain, allowToAdd: Boolean,
|
||||
added: Boolean, canBeRemoved: Boolean,
|
||||
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClicked: (ContractAddress) -> Unit
|
||||
) {
|
||||
|
||||
val isBlockchain = contract == null || contract.address == currency.symbol
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
enabled = allowToAdd,
|
||||
onLongClick = {
|
||||
if (contract != null) onNetworkItemClicked(contract.address)
|
||||
contract.address?.let { onNetworkItemClicked(it) }
|
||||
},
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
|
||||
contentDescription = blockchain.fullName,
|
||||
loading = { CurrencyPlaceholderIcon(blockchain.id) },
|
||||
error = { CurrencyPlaceholderIcon(blockchain.id) },
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
|
||||
.size(20.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
|
||||
contentDescription = blockchain.fullName,
|
||||
loading = { CurrencyPlaceholderIcon(blockchain.id) },
|
||||
error = { CurrencyPlaceholderIcon(blockchain.id) },
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
)
|
||||
if (contract.address == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(7.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF1ACE80))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
|
|
@ -71,36 +93,32 @@ fun NetworkItem(
|
|||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = blockchain.name.uppercase(),
|
||||
fontSize = 13.sp,
|
||||
text = prepareNetworkNameSpannableText(
|
||||
blockchain = blockchain,
|
||||
contractAddress = contract.address
|
||||
),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (added) Color.Black else Color(0xFF848488),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(3.dp))
|
||||
Text(
|
||||
text = if (isBlockchain) "MAIN" else blockchain.getNetworkName().uppercase(),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (!isBlockchain) Color(0xFF8E8E93) else Color(0xFF1ACE80),
|
||||
color = if (added) Color.Black else Color(0xFF848488),
|
||||
)
|
||||
}
|
||||
|
||||
if (allowToAdd) {
|
||||
val token = if (!isBlockchain) {
|
||||
val token = if (contract.address != null) {
|
||||
Token(
|
||||
id = currency.id,
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = contract!!.address,
|
||||
decimals = contract.decimalCount,
|
||||
contractAddress = contract.address,
|
||||
decimals = contract.decimalCount!!,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val tokenWithBlockchain =
|
||||
token?.let { TokenWithBlockchain(token, contract!!.blockchain) }
|
||||
token?.let { TokenWithBlockchain(token, contract.blockchain) }
|
||||
|
||||
val currencyToSave = if (isBlockchain && contract != null) {
|
||||
val currencyToSave = if (contract.address == null) {
|
||||
currency.copy(id = contract.networkId)
|
||||
} else {
|
||||
currency
|
||||
|
|
@ -117,4 +135,33 @@ fun NetworkItem(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun prepareNetworkNameSpannableText(
|
||||
blockchain: Blockchain,
|
||||
contractAddress: String?
|
||||
): AnnotatedString {
|
||||
|
||||
val blockchainName = blockchain.fullNameWithoutTestnet.uppercase()
|
||||
val additionalText =
|
||||
if (contractAddress == null) "MAIN" else blockchain.getNetworkName().uppercase()
|
||||
|
||||
val text = "$blockchainName $additionalText"
|
||||
|
||||
val startOfAdditionalText =
|
||||
if (additionalText.isNotBlank()) text.indexOf(additionalText) else text.length
|
||||
|
||||
val spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (contractAddress != null) Color(0xFF8E8E93) else Color(0xFF1ACE80)
|
||||
),
|
||||
start = startOfAdditionalText,
|
||||
end = text.length
|
||||
)
|
||||
)
|
||||
return AnnotatedString(text = text, spanStyles = spanStyles)
|
||||
}
|
||||
|
|
@ -58,8 +58,8 @@ sealed class WalletAction : Action {
|
|||
|
||||
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
|
||||
data class SaveCurrencies(val blockchainNetworks: List<BlockchainNetwork>) : MultiWallet()
|
||||
object FindTokensInUse : MultiWallet()
|
||||
object FindBlockchainsInUse : MultiWallet()
|
||||
// object FindTokensInUse : MultiWallet()
|
||||
// object FindBlockchainsInUse : MultiWallet()
|
||||
|
||||
data class TokenLoaded(
|
||||
val amount: Amount,
|
||||
|
|
@ -91,10 +91,12 @@ sealed class WalletAction : Action {
|
|||
}
|
||||
|
||||
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
|
||||
|
||||
object RestoreFundsWarningClosed : Warnings()
|
||||
}
|
||||
|
||||
data class LoadFiatRate(
|
||||
val wallet: Wallet? = null, val currencyList: List<Currency>? = null,
|
||||
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
|
||||
) : WalletAction() {
|
||||
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
|
||||
object Failure : WalletAction()
|
||||
|
|
@ -159,4 +161,6 @@ sealed class WalletAction : Action {
|
|||
val minRent: String,
|
||||
val rentExempt: String
|
||||
) : WalletAction()
|
||||
|
||||
data class RemoveWalletRent(val blockchain: BlockchainNetwork) : WalletAction()
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@ 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.TapWorkarounds.derivationStyle
|
||||
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.extensions.toQrCode
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
|
|
@ -17,6 +18,7 @@ import com.tangem.tap.domain.extensions.sellIsAllowed
|
|||
import com.tangem.tap.domain.extensions.toSendableAmounts
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
||||
import com.tangem.tap.features.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
|
||||
|
|
@ -59,7 +61,7 @@ data class WalletState(
|
|||
|
||||
val shouldShowDetails: Boolean =
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
|
||||
|
||||
val blockchains: List<Blockchain>
|
||||
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
|
||||
|
|
@ -73,19 +75,9 @@ data class WalletState(
|
|||
val walletManagers: List<WalletManager>
|
||||
get() = wallets.mapNotNull { it.walletManager }
|
||||
|
||||
fun getWalletManager(token: Token?): WalletManager? {
|
||||
if (token == null) return null
|
||||
return wallets
|
||||
.mapNotNull { it.walletManager }
|
||||
.find { walletManager ->
|
||||
walletManager.cardTokens.any { it.contractAddress == token.contractAddress }
|
||||
}
|
||||
}
|
||||
|
||||
fun getWalletManager(currency: Currency?): WalletManager? {
|
||||
if (currency?.blockchain == null) return null
|
||||
return wallets.map { it.walletManager }
|
||||
.find { it?.wallet?.blockchain == currency.blockchain }
|
||||
return getWalletStore(currency)?.walletManager
|
||||
}
|
||||
|
||||
fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? {
|
||||
|
|
@ -96,8 +88,8 @@ data class WalletState(
|
|||
if (blockchain == null) return null
|
||||
return walletsData.find {
|
||||
it.currency is Currency.Blockchain &&
|
||||
it.currency.blockchain == blockchain.blockchain &&
|
||||
it.currency.derivationPath == blockchain.derivationPath
|
||||
it.currency.blockchain == blockchain.blockchain &&
|
||||
it.currency.derivationPath == blockchain.derivationPath
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +97,7 @@ data class WalletState(
|
|||
if (currency == null) return null
|
||||
return wallets.firstOrNull {
|
||||
it.blockchainNetwork.derivationPath == currency.derivationPath &&
|
||||
(it.blockchainNetwork.blockchain == currency.blockchain)
|
||||
(it.blockchainNetwork.blockchain == currency.blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +112,7 @@ data class WalletState(
|
|||
if (blockchainNetwork == null) return null
|
||||
return wallets.firstOrNull {
|
||||
it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath &&
|
||||
(it.blockchainNetwork.blockchain == blockchainNetwork.blockchain)
|
||||
(it.blockchainNetwork.blockchain == blockchainNetwork.blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,22 +121,6 @@ data class WalletState(
|
|||
return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency }
|
||||
}
|
||||
|
||||
fun getWalletData(token: Token?): WalletData? {
|
||||
if (token == null) return null
|
||||
return walletsData.find {
|
||||
(it.currency as? Currency.Token)?.token == token &&
|
||||
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
|
||||
}
|
||||
}
|
||||
|
||||
fun getWalletData(blockchain: Blockchain?): WalletData? {
|
||||
if (blockchain == null) return null
|
||||
return walletsData.find {
|
||||
(it.currency as? Currency.Blockchain)?.blockchain == blockchain &&
|
||||
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedWalletData(): WalletData? {
|
||||
return walletsData.find { it.currency == selectedCurrency }
|
||||
}
|
||||
|
|
@ -166,23 +142,23 @@ data class WalletState(
|
|||
|
||||
if (walletData.currency is Currency.Blockchain) {
|
||||
return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() &&
|
||||
wallet.amounts.toSendableAmounts().isEmpty()
|
||||
wallet.amounts.toSendableAmounts().isEmpty()
|
||||
} else if (walletData.currency is Currency.Token) (
|
||||
return wallet.recentTransactions.toPendingTransactionsForToken(
|
||||
walletData.currency.token, wallet.address
|
||||
).isEmpty()
|
||||
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
|
||||
?.isAboveZero() != true
|
||||
)
|
||||
return wallet.recentTransactions.toPendingTransactionsForToken(
|
||||
walletData.currency.token, wallet.address
|
||||
).isEmpty()
|
||||
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
|
||||
?.isAboveZero() != true
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
|
||||
return (walletData.currency is Currency.Blockchain &&
|
||||
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|
||||
|| (walletData.currency is Currency.Token &&
|
||||
walletData.currency.token == store.state.walletState.primaryToken)
|
||||
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|
||||
|| (walletData.currency is Currency.Token &&
|
||||
walletData.currency.token == store.state.walletState.primaryToken)
|
||||
}
|
||||
|
||||
fun replaceWalletInWallets(wallet: WalletStore?): List<WalletStore> {
|
||||
|
|
@ -239,7 +215,7 @@ data class WalletState(
|
|||
return if (walletData.currency is Currency.Blockchain) {
|
||||
val walletStores = wallets.filterNot {
|
||||
it.blockchainNetwork.blockchain == walletData.currency.blockchain
|
||||
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
|
||||
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
|
||||
}
|
||||
copy(wallets = walletStores)
|
||||
} else {
|
||||
|
|
@ -396,6 +372,11 @@ data class WalletRent(
|
|||
|
||||
sealed interface Currency {
|
||||
|
||||
val coinId: String?
|
||||
get() = when (this) {
|
||||
is Blockchain -> blockchain.toCoinId()
|
||||
is Token -> token.id
|
||||
}
|
||||
val blockchain: com.tangem.blockchain.common.Blockchain
|
||||
val currencySymbol: CryptoCurrencyName
|
||||
val derivationPath: String?
|
||||
|
|
@ -416,10 +397,16 @@ sealed interface Currency {
|
|||
}
|
||||
|
||||
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
|
||||
if (this is Token && this.token.id == null) return true
|
||||
|
||||
if (derivationPath == null || derivationStyle == null) return false
|
||||
|
||||
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
}
|
||||
|
||||
fun isBlockchain(): Boolean = this is Blockchain
|
||||
|
||||
fun isToken(): Boolean = this is Token
|
||||
|
||||
companion object {
|
||||
fun fromBlockchainNetwork(
|
||||
|
|
@ -427,18 +414,40 @@ sealed interface Currency {
|
|||
token: com.tangem.blockchain.common.Token? = null
|
||||
): Currency {
|
||||
return if (token != null) {
|
||||
Currency.Token(
|
||||
Token(
|
||||
token = token,
|
||||
blockchain = blockchainNetwork.blockchain,
|
||||
derivationPath = blockchainNetwork.derivationPath
|
||||
)
|
||||
} else {
|
||||
Currency.Blockchain(
|
||||
Blockchain(
|
||||
blockchain = blockchainNetwork.blockchain,
|
||||
derivationPath = blockchainNetwork.derivationPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromCustomCurrency(customCurrency: CustomCurrency): Currency {
|
||||
return when (customCurrency) {
|
||||
is CustomCurrency.CustomBlockchain -> Blockchain(
|
||||
blockchain = customCurrency.network,
|
||||
derivationPath = customCurrency.derivationPath?.rawPath
|
||||
)
|
||||
is CustomCurrency.CustomToken -> Token(
|
||||
token = customCurrency.token,
|
||||
blockchain = customCurrency.network,
|
||||
derivationPath = customCurrency.derivationPath?.rawPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token {
|
||||
return Token(
|
||||
token = tokenWithBlockchain.token,
|
||||
blockchain = tokenWithBlockchain.blockchain,
|
||||
derivationPath = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -450,7 +459,7 @@ data class WalletStore(
|
|||
fun updateWallets(walletDataList: List<WalletData>): WalletStore {
|
||||
val relevantWalletDataList = walletDataList.filter {
|
||||
it.currency.blockchain == blockchainNetwork.blockchain &&
|
||||
it.currency.derivationPath == blockchainNetwork.derivationPath
|
||||
it.currency.derivationPath == blockchainNetwork.derivationPath
|
||||
}.toMutableList()
|
||||
val updatedWalletDataList = walletsData.map { walletData ->
|
||||
val matchingWalletData = relevantWalletDataList.find { it.currency == walletData.currency }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
package com.tangem.tap.features.wallet.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
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.extensions.makeWalletManagersForApp
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
|
|
@ -19,7 +18,6 @@ import com.tangem.tap.features.wallet.redux.WalletState
|
|||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -29,7 +27,7 @@ class MultiWalletMiddleware {
|
|||
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?,
|
||||
) {
|
||||
val globalState = globalState ?: return
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
// val tapWalletManager = globalState.tapWalletManager
|
||||
|
||||
when (action) {
|
||||
is WalletAction.MultiWallet.AddBlockchains -> {
|
||||
|
|
@ -57,19 +55,17 @@ class MultiWalletMiddleware {
|
|||
blockchainNetwork = action.blockchain
|
||||
)
|
||||
}
|
||||
store.dispatch(
|
||||
WalletAction.LoadFiatRate(
|
||||
currencyList = listOf(
|
||||
Currency.Blockchain(
|
||||
action.blockchain.blockchain,
|
||||
action.blockchain.derivationPath
|
||||
)
|
||||
store.dispatch(WalletAction.LoadFiatRate(
|
||||
coinsList = listOf(
|
||||
Currency.Blockchain(
|
||||
action.blockchain.blockchain,
|
||||
action.blockchain.derivationPath
|
||||
)
|
||||
)
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet(action.blockchain, action.walletManager)
|
||||
)
|
||||
))
|
||||
store.dispatch(WalletAction.LoadWallet(
|
||||
action.blockchain, action.walletManager
|
||||
))
|
||||
}
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> {
|
||||
globalState.scanResponse?.card?.cardId?.let {
|
||||
|
|
@ -91,7 +87,7 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val walletManager = walletState?.getWalletManager(currency.token)
|
||||
val walletManager = walletState?.getWalletManager(currency)
|
||||
if (walletManager != null) {
|
||||
walletManager.removeToken(currency.token)
|
||||
cardId?.let {
|
||||
|
|
@ -105,98 +101,94 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.FindBlockchainsInUse -> {
|
||||
val scanResponse = globalState.scanResponse ?: return
|
||||
if (scanResponse.supportsHdWallet()) return
|
||||
|
||||
val cardFirmware = scanResponse.card.firmwareVersion
|
||||
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
|
||||
.filterNot { walletState?.blockchains?.contains(it) == true }
|
||||
.map { BlockchainNetwork(it, null, emptyList()) }
|
||||
val walletManagers =
|
||||
tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
|
||||
scanResponse,
|
||||
blockchains
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
walletManagers.map { walletManager ->
|
||||
async(Dispatchers.IO) {
|
||||
walletManager.safeUpdate()
|
||||
val wallet = walletManager.wallet
|
||||
val coinAmount = wallet.amounts[AmountType.Coin]?.value
|
||||
if (coinAmount != null && !coinAmount.isZero()) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
if (walletState?.getWalletData(blockchainNetwork) == null) {
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchainNetwork, walletManager
|
||||
)
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet.Success(
|
||||
wallet = wallet,
|
||||
blockchain = blockchainNetwork
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.FindTokensInUse -> {
|
||||
val scanResponse = globalState.scanResponse ?: return
|
||||
if (scanResponse.supportsHdWallet()) return
|
||||
|
||||
val walletFactory = tapWalletManager.walletManagerFactory
|
||||
val card = scanResponse.card
|
||||
|
||||
val walletManager = walletState?.getWalletManager(
|
||||
Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
)
|
||||
?: walletFactory.makeWalletManagerForApp(
|
||||
scanResponse,
|
||||
Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
)
|
||||
|
||||
val tokenFinder = walletManager as? TokenFinder ?: return
|
||||
scope.launch {
|
||||
val result = tokenFinder.findTokens()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
if (result.data.isNotEmpty()) {
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
walletManager.wallet.blockchain,
|
||||
walletManager.wallet.publicKey.derivationPath?.rawPath,
|
||||
walletManager.cardTokens.toList()
|
||||
)
|
||||
currenciesRepository.saveUpdatedCurrency(
|
||||
card.cardId,
|
||||
blockchainNetwork
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchainNetwork,
|
||||
walletManager
|
||||
)
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddTokens(
|
||||
walletManager.cardTokens.toList(),
|
||||
blockchainNetwork
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// is WalletAction.MultiWallet.FindBlockchainsInUse -> {
|
||||
// val scanResponse = globalState.scanResponse ?: return
|
||||
// if (scanResponse.supportsHdWallet()) return
|
||||
//
|
||||
// val cardFirmware = scanResponse.card.firmwareVersion
|
||||
// val blockchains = currenciesRepository.getBlockchains(cardFirmware)
|
||||
// .filterNot { walletState?.blockchains?.contains(it) == true }
|
||||
// .map { BlockchainNetwork(it, null, emptyList()) }
|
||||
// val walletManagers =
|
||||
// tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
|
||||
// scanResponse,
|
||||
// blockchains
|
||||
// )
|
||||
//
|
||||
// scope.launch {
|
||||
// walletManagers.map { walletManager ->
|
||||
// async(Dispatchers.IO) {
|
||||
// walletManager.safeUpdate()
|
||||
// val wallet = walletManager.wallet
|
||||
// val coinAmount = wallet.amounts[AmountType.Coin]?.value
|
||||
// if (coinAmount != null && !coinAmount.isZero()) {
|
||||
// scope.launch(Dispatchers.Main) {
|
||||
// val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
// if (walletState?.getWalletData(blockchainNetwork) == null) {
|
||||
// store.dispatch(WalletAction.MultiWallet.AddBlockchain(
|
||||
// blockchainNetwork, walletManager
|
||||
// ))
|
||||
// store.dispatch(WalletAction.LoadWallet.Success(
|
||||
// wallet = wallet,
|
||||
// blockchain = blockchainNetwork
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// is WalletAction.MultiWallet.FindTokensInUse -> {
|
||||
// val scanResponse = globalState.scanResponse ?: return
|
||||
// if (scanResponse.supportsHdWallet()) return
|
||||
//
|
||||
// val walletFactory = tapWalletManager.walletManagerFactory
|
||||
// val card = scanResponse.card
|
||||
//
|
||||
// val walletManager = walletState?.getWalletManager(
|
||||
// Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
// )
|
||||
// ?: walletFactory.makeWalletManagerForApp(
|
||||
// scanResponse,
|
||||
// Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
// )
|
||||
//
|
||||
// val tokenFinder = walletManager as? TokenFinder ?: return
|
||||
// scope.launch {
|
||||
// val result = tokenFinder.findTokens()
|
||||
//
|
||||
// withContext(Dispatchers.Main) {
|
||||
// when (result) {
|
||||
// is Result.Success -> {
|
||||
// if (result.data.isNotEmpty()) {
|
||||
// val blockchainNetwork = BlockchainNetwork(
|
||||
// walletManager.wallet.blockchain,
|
||||
// walletManager.wallet.publicKey.derivationPath?.rawPath,
|
||||
// walletManager.cardTokens.toList()
|
||||
// )
|
||||
// currenciesRepository.saveUpdatedCurrency(
|
||||
// card.cardId,
|
||||
// blockchainNetwork
|
||||
// )
|
||||
// store.dispatch(
|
||||
// WalletAction.MultiWallet.AddBlockchain(
|
||||
// blockchainNetwork,
|
||||
// walletManager
|
||||
// )
|
||||
// )
|
||||
// store.dispatch(
|
||||
// WalletAction.MultiWallet.AddTokens(
|
||||
// walletManager.cardTokens.toList(),
|
||||
// blockchainNetwork
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +214,7 @@ class MultiWalletMiddleware {
|
|||
tokens: List<Token>, blockchainNetwork: BlockchainNetwork,
|
||||
walletState: WalletState?, globalState: GlobalState?
|
||||
) {
|
||||
if (tokens.isEmpty()) return
|
||||
val scanResponse = globalState?.scanResponse ?: return
|
||||
val wmFactory = globalState.tapWalletManager.walletManagerFactory
|
||||
|
||||
|
|
@ -230,12 +223,13 @@ class MultiWalletMiddleware {
|
|||
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, it))
|
||||
} ?: return
|
||||
|
||||
store.dispatch(WalletAction.LoadFiatRate(currencyList = tokens.map { token ->
|
||||
store.dispatch(WalletAction.LoadFiatRate(coinsList = tokens.map { token ->
|
||||
Currency.Token(
|
||||
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
|
||||
)
|
||||
}))
|
||||
walletManager.addTokens(tokens)
|
||||
if (tokens.isNotEmpty()) walletManager.addTokens(tokens)
|
||||
|
||||
currenciesRepository.saveUpdatedCurrency(
|
||||
cardId = scanResponse.card.cardId,
|
||||
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class TradeCryptoMiddleware {
|
|||
if (exchangeAction == CurrencyExchangeManager.Action.Buy &&
|
||||
currency is Currency.Token && currency.blockchain.isTestnet()
|
||||
) {
|
||||
val walletManager = store.state.walletState.getWalletManager(currency.token)
|
||||
val walletManager = store.state.walletState.getWalletManager(currency)
|
||||
if (walletManager !is EthereumWalletManager) return
|
||||
|
||||
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package com.tangem.tap.features.wallet.redux.middlewares
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.isZero
|
||||
|
|
@ -20,9 +22,14 @@ 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.redux.*
|
||||
import com.tangem.tap.network.NetworkStateChanged
|
||||
import com.tangem.tap.scope
|
||||
|
|
@ -34,6 +41,8 @@ import kotlinx.coroutines.launch
|
|||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WalletMiddleware {
|
||||
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
|
||||
|
|
@ -77,49 +86,52 @@ class WalletMiddleware {
|
|||
}
|
||||
}
|
||||
is WalletAction.LoadWallet.Success -> {
|
||||
checkForRentWarning(walletState.getWalletManager(action.blockchain))
|
||||
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
|
||||
if (coinAmount != null && !coinAmount.isZero()) {
|
||||
if (walletState.getWalletData(action.blockchain) == null) {
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
action.blockchain,
|
||||
null
|
||||
)
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet.Success(
|
||||
action.wallet,
|
||||
action.blockchain
|
||||
)
|
||||
)
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
|
||||
action.blockchain,
|
||||
null
|
||||
))
|
||||
store.dispatch(WalletAction.LoadWallet.Success(
|
||||
action.wallet,
|
||||
action.blockchain
|
||||
))
|
||||
}
|
||||
}
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
|
||||
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
|
||||
}
|
||||
is WalletAction.LoadFiatRate -> {
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
val fiatAppCurrency = globalState.appCurrency
|
||||
val appCurrencyId = globalState.appCurrency
|
||||
scope.launch {
|
||||
when {
|
||||
val coinsList = when {
|
||||
action.wallet != null -> {
|
||||
globalState.tapWalletManager.loadFiatRate(
|
||||
fiatCurrency = fiatAppCurrency,
|
||||
wallet = action.wallet,
|
||||
)
|
||||
val wallet = action.wallet
|
||||
wallet.getTokens()
|
||||
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
|
||||
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
|
||||
}
|
||||
action.currencyList != null -> {
|
||||
globalState.tapWalletManager.loadFiatRate(
|
||||
fiatCurrency = fiatAppCurrency,
|
||||
currencies = action.currencyList,
|
||||
)
|
||||
action.coinsList != null -> action.coinsList
|
||||
else -> walletState.walletsData.map { it.currency }
|
||||
}
|
||||
val ratesResult = globalState.tapWalletManager.rates.loadFiatRate(
|
||||
currencyId = appCurrencyId,
|
||||
coinsList = coinsList,
|
||||
)
|
||||
when (ratesResult) {
|
||||
is Result.Success -> {
|
||||
ratesResult.data.loadedRates.forEach {
|
||||
dispatchOnMain(WalletAction.LoadFiatRate.Success(it.toPair()))
|
||||
}
|
||||
ratesResult.data.failedRates.forEach { (currency, throwable) ->
|
||||
Timber.e(throwable, "Loading rates failed for [%s]", currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val currencyList = walletState.walletsData.map { it.currency }
|
||||
globalState.tapWalletManager.loadFiatRate(
|
||||
fiatCurrency = fiatAppCurrency,
|
||||
currencies = currencyList,
|
||||
)
|
||||
is Result.Failure -> {
|
||||
store.dispatchDebugErrorNotification("LoadFiatRate.Failure")
|
||||
dispatchOnMain(WalletAction.LoadFiatRate.Failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -293,4 +305,43 @@ class WalletMiddleware {
|
|||
tokenRate = tokenRate
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkForRentWarning(walletManager: WalletManager?) {
|
||||
val rentProvider = walletManager as? RentProvider ?: return
|
||||
|
||||
scope.launch {
|
||||
when (val result = rentProvider.minimalBalanceForRentExemption()) {
|
||||
is com.tangem.blockchain.extensions.Result.Success -> {
|
||||
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean {
|
||||
return balance < rentExempt
|
||||
}
|
||||
|
||||
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
|
||||
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
|
||||
val rentExempt = result.data
|
||||
val show = if (outgoingTxs.isEmpty()) {
|
||||
isNeedToShowWarning(balance, rentExempt)
|
||||
} else {
|
||||
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
|
||||
val rest = balance.minus(outgoingAmount)
|
||||
isNeedToShowWarning(rest, rentExempt)
|
||||
}
|
||||
|
||||
val currency = walletManager.wallet.blockchain.currency
|
||||
if (show) {
|
||||
dispatchOnMain(WalletAction.SetWalletRent(
|
||||
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
|
||||
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
|
||||
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
|
||||
))
|
||||
} else {
|
||||
dispatchOnMain(WalletAction.RemoveWalletRent(
|
||||
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
|
||||
))
|
||||
}
|
||||
}
|
||||
is com.tangem.blockchain.extensions.Result.Failure -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import com.tangem.common.card.Card
|
|||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.isGreaterThan
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
|
|
@ -64,6 +66,9 @@ class WarningsMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
is WalletAction.Warnings.RestoreFundsWarningClosed -> {
|
||||
preferencesStorage.saveRestoreFundsWarningClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +93,9 @@ class WarningsMiddleware {
|
|||
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
|
||||
return@let
|
||||
}
|
||||
if (card.useOldStyleDerivation && !preferencesStorage.wasRestoreFundsWarningClosed()) {
|
||||
addWarningMessage(warning = WarningMessagesManager.restoreFundsWarning())
|
||||
}
|
||||
|
||||
showWarningLowRemainingSignaturesIfNeeded(card)
|
||||
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
|
||||
|
|
@ -193,7 +201,7 @@ class WarningsMiddleware {
|
|||
}
|
||||
|
||||
private fun setWarningMessages() {
|
||||
store.dispatch(WalletAction.Warnings.Set(getWarnings()))
|
||||
store.dispatchOnMain(WalletAction.Warnings.Set(getWarnings()))
|
||||
}
|
||||
|
||||
private fun getWarnings(): List<WarningMessage> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.reducers
|
|||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
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
|
||||
|
|
@ -14,16 +15,17 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
|
|||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.features.wallet.ui.TokenData
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
|
||||
class MultiWalletReducer {
|
||||
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
|
||||
return when (action) {
|
||||
is WalletAction.MultiWallet.AddBlockchains -> {
|
||||
val wallets: List<WalletStore> = action.blockchains.map { blockchain ->
|
||||
val walletManager = action.walletManagers.first {
|
||||
val wallets: List<WalletStore> = action.blockchains.mapNotNull { blockchain ->
|
||||
val walletManager = action.walletManagers.firstOrNull {
|
||||
it.wallet.blockchain == blockchain.blockchain &&
|
||||
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
|
||||
}
|
||||
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
|
||||
} ?: return@mapNotNull null
|
||||
val wallet = walletManager.wallet
|
||||
val cardToken = if (!state.isMultiwalletAllowed) {
|
||||
wallet.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
|
||||
|
|
@ -99,11 +101,12 @@ class MultiWalletReducer {
|
|||
addTokens(listOf(action.token), action.blockchain, state)
|
||||
}
|
||||
is WalletAction.MultiWallet.TokenLoaded -> {
|
||||
val pendingTransactions = state.getWalletManager(action.token)
|
||||
?.wallet?.let { wallet ->
|
||||
wallet.recentTransactions.toPendingTransactions(wallet.address)
|
||||
} ?: emptyList()
|
||||
val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token)
|
||||
val wallet = state.getWalletManager(currency)?.wallet.guard {
|
||||
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
|
||||
|
|
@ -113,7 +116,7 @@ class MultiWalletReducer {
|
|||
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
|
||||
else -> BalanceStatus.VerifiedOnline
|
||||
}
|
||||
val tokenWalletData = state.getWalletData(action.token)
|
||||
val tokenWalletData = state.getWalletData(currency)
|
||||
val newTokenWalletData = tokenWalletData?.copy(
|
||||
currencyData = tokenWalletData.currencyData.copy(
|
||||
status = tokenBalanceStatus,
|
||||
|
|
@ -124,7 +127,8 @@ class MultiWalletReducer {
|
|||
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
|
||||
action.amount.value
|
||||
?.toFiatString(it, store.state.globalState.appCurrency)
|
||||
}
|
||||
},
|
||||
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
|
||||
),
|
||||
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
|
||||
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
|
||||
|
|
@ -150,8 +154,8 @@ class MultiWalletReducer {
|
|||
|
||||
is WalletAction.MultiWallet.SetPrimaryToken ->
|
||||
state.copy(primaryToken = action.token)
|
||||
is WalletAction.MultiWallet.FindTokensInUse -> state
|
||||
is WalletAction.MultiWallet.FindBlockchainsInUse -> state
|
||||
// is WalletAction.MultiWallet.FindTokensInUse -> state
|
||||
// is WalletAction.MultiWallet.FindBlockchainsInUse -> state
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> state
|
||||
}
|
||||
}
|
||||
|
|
@ -166,11 +170,10 @@ private fun addTokens(
|
|||
|
||||
fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? {
|
||||
if (!state.isMultiwalletAllowed) return null
|
||||
if (state.currencies.any { it is Currency.Token && it.token == this }) {
|
||||
return null
|
||||
}
|
||||
val currency = Currency.fromBlockchainNetwork(blockchain, this)
|
||||
if (state.currencies.contains(currency)) return null
|
||||
|
||||
val walletManager = state.getWalletManager(this)?.wallet
|
||||
val walletManager = state.getWalletManager(currency)?.wallet
|
||||
val walletAddresses = createAddressList(walletManager)
|
||||
|
||||
return WalletData(
|
||||
|
|
@ -181,6 +184,6 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
|
|||
),
|
||||
walletAddresses = walletAddresses,
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
currency = Currency.fromBlockchainNetwork(blockchain, this)
|
||||
currency = currency
|
||||
)
|
||||
}
|
||||
|
|
@ -74,7 +74,8 @@ class OnWalletLoadedReducer {
|
|||
)
|
||||
|
||||
val tokens = wallet.getTokens().mapNotNull { token ->
|
||||
val tokenWalletData = walletState.getWalletData(token)
|
||||
val currency = Currency.fromBlockchainNetwork(blockchainNetwork, token)
|
||||
val tokenWalletData = walletState.getWalletData(currency)
|
||||
val tokenPendingTransactions =
|
||||
pendingTransactions.filter { it.currency == token.symbol }
|
||||
val tokenBalanceStatus = when {
|
||||
|
|
|
|||
|
|
@ -164,7 +164,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
|
||||
)
|
||||
}
|
||||
val wallets = newState.updateTradeCryptoState(exchangeManager, newState.replaceSomeWallets(newWallets))
|
||||
val wallets = newState.updateTradeCryptoState(
|
||||
exchangeManager,
|
||||
newState.replaceSomeWallets(newWallets)
|
||||
)
|
||||
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets)
|
||||
newState = newState.updateWalletStore(walletStore)
|
||||
}
|
||||
|
|
@ -217,7 +220,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
),
|
||||
)
|
||||
val tokenWallets = action.wallet.getTokens()
|
||||
.mapNotNull { newState.getWalletData(it) }
|
||||
.mapNotNull { token ->
|
||||
walletStore?.blockchainNetwork?.let {
|
||||
newState.getWalletData(Currency.fromBlockchainNetwork(it, token))
|
||||
}
|
||||
}
|
||||
.map {
|
||||
it.copy(
|
||||
currencyData = it.currencyData.copy(
|
||||
|
|
@ -288,12 +295,14 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
?: return newState
|
||||
val address = walletAddresses.list.firstOrNull { it.type == action.type }
|
||||
?: return newState
|
||||
newState = newState.updateWalletData(selectedWalletData?.copy(
|
||||
walletAddresses = WalletAddresses(
|
||||
address,
|
||||
walletAddresses.list
|
||||
newState = newState.updateWalletData(
|
||||
selectedWalletData?.copy(
|
||||
walletAddresses = WalletAddresses(
|
||||
address,
|
||||
walletAddresses.list
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
}
|
||||
is WalletAction.SetWalletRent -> {
|
||||
var walletData = newState.getWalletData(action.blockchain)
|
||||
|
|
@ -305,6 +314,15 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
newState = newState.updateWalletsData(listOf(walletData))
|
||||
}
|
||||
}
|
||||
is WalletAction.RemoveWalletRent -> {
|
||||
var walletData = newState.getWalletData(action.blockchain)
|
||||
if (walletData == null) {
|
||||
newState
|
||||
} else {
|
||||
walletData = walletData.copy(warningRent = null)
|
||||
newState = newState.updateWalletsData(listOf(walletData))
|
||||
}
|
||||
}
|
||||
}
|
||||
return newState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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.redux.navigation.NavigationAction
|
||||
|
|
@ -24,6 +25,7 @@ import com.tangem.tap.features.wallet.models.PendingTransaction
|
|||
import com.tangem.tap.features.wallet.redux.*
|
||||
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
|
||||
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
|
||||
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
|
||||
|
|
@ -54,9 +56,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.walletState == newState.walletState
|
||||
}.select { it.walletState }
|
||||
state.select { it.walletState }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,12 +70,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
|
||||
|
||||
|
||||
setupTransactionsRecyclerView()
|
||||
setupButtons()
|
||||
setupTestActionButton()
|
||||
}
|
||||
|
||||
|
||||
private fun setupTransactionsRecyclerView() = with(binding) {
|
||||
pendingTransactionAdapter = PendingTransactionsAdapter()
|
||||
rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
|
@ -93,6 +92,15 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
btnSell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
|
||||
}
|
||||
|
||||
private fun setupTestActionButton() {
|
||||
view?.findViewById<View>(R.id.l_balance)?.let { view ->
|
||||
TestActions.initFor(
|
||||
view = view,
|
||||
actions = TestWalletDetails.solanaRentExemptWarning()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newState(state: WalletState) {
|
||||
if (activity == null || view == null) return
|
||||
if (state.selectedCurrency == null) return
|
||||
|
|
@ -113,11 +121,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
|
||||
binding.srlWalletDetails.setOnRefreshListener {
|
||||
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet(
|
||||
blockchain = BlockchainNetwork(selectedWallet.currency.blockchain, selectedWallet.currency.derivationPath, emptyList())
|
||||
store.dispatch(WalletAction.LoadWallet(
|
||||
blockchain = BlockchainNetwork(
|
||||
selectedWallet.currency.blockchain,
|
||||
selectedWallet.currency.derivationPath,
|
||||
emptyList()
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +175,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
}
|
||||
|
||||
private fun handleNotEnoughFundsOnMainCurrency(selectedWalletData: WalletData) = with(binding) {
|
||||
if (selectedWalletData.currency.isBlockchain()) return@with
|
||||
|
||||
if (selectedWalletData.shouldShowCoinAmountWarning()) {
|
||||
val blockchainName = selectedWalletData.currency.blockchain.fullName
|
||||
val warningMessage = requireContext().getString(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ 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.configurable.warningMessage.WarningMessage
|
||||
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.*
|
||||
|
|
@ -29,6 +31,7 @@ import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
|
|||
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.WalletView
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
|
@ -80,6 +83,16 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
setupWarningsRecyclerView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
// addCustomActionOnCard()
|
||||
}
|
||||
|
||||
private fun addCustomActionOnCard() {
|
||||
if (!BuildConfig.TEST_ACTION_ENABLED) return
|
||||
|
||||
binding.ivCard.setOnClickListener {
|
||||
printScanResponseState()
|
||||
printWalletState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWarningsRecyclerView() {
|
||||
|
|
@ -151,10 +164,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
|
||||
private fun setupCardImage(cardImage: Artwork?) {
|
||||
Picasso.get()
|
||||
.load(cardImage?.artworkId)
|
||||
.placeholder(R.drawable.card_placeholder_black)
|
||||
?.error(R.drawable.card_placeholder_black)
|
||||
?.into(binding.ivCard)
|
||||
.load(cardImage?.artworkId)
|
||||
.placeholder(R.drawable.card_placeholder_black)
|
||||
?.error(R.drawable.card_placeholder_black)
|
||||
?.into(binding.ivCard)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
|
@ -166,7 +179,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
scanNoteResponse,
|
||||
store.state.walletState.walletManagers.map { it.wallet },
|
||||
CardTou(),
|
||||
store.state.globalState.appCurrency
|
||||
store.state.globalState.appCurrency,
|
||||
tangemTechService = store.state.domainNetworks.tangemTechService
|
||||
))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
|
||||
true
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
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.features.wallet.redux.Currency
|
||||
|
|
@ -85,36 +86,42 @@ class WalletAdapter
|
|||
)
|
||||
|
||||
when (wallet.currencyData.status) {
|
||||
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning()
|
||||
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning(isCustom)
|
||||
BalanceStatus.Loading -> {
|
||||
hideWarning()
|
||||
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))
|
||||
showWarning(root.getString(R.string.wallet_balance_tx_in_progress), isCustom)
|
||||
BalanceStatus.Unreachable ->
|
||||
showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable))
|
||||
showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable), isCustom)
|
||||
|
||||
BalanceStatus.NoAccount ->
|
||||
showWarning(root.getString(R.string.wallet_error_no_account))
|
||||
showWarning(root.getString(R.string.wallet_error_no_account), isCustom)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showWarning(message: String) {
|
||||
toggleWarning(true)
|
||||
private fun showWarning(message: String, isCustom: Boolean = false) {
|
||||
toggleWarning(true, isCustom)
|
||||
binding.tvStatusErrorMessage.text = message
|
||||
}
|
||||
|
||||
private fun hideWarning() {
|
||||
toggleWarning(false)
|
||||
private fun hideWarning(isCustom: Boolean = false) {
|
||||
toggleWarning(false, isCustom)
|
||||
}
|
||||
|
||||
private fun toggleWarning(show: Boolean) {
|
||||
binding.tvExchangeRate.show(!show)
|
||||
private fun toggleWarning(show: Boolean, isCustom: Boolean = false) {
|
||||
if (!show) {
|
||||
binding.tvExchangeRate.show(!isCustom)
|
||||
binding.tvCustomCurrency.show(isCustom)
|
||||
} else {
|
||||
binding.tvExchangeRate.hide()
|
||||
binding.tvCustomCurrency.hide()
|
||||
}
|
||||
binding.tvStatusErrorMessage.show(show)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
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
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
|
@ -13,6 +15,7 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
|
|||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -56,7 +59,9 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
|
|||
|
||||
tvTitle.text = getString(warning.titleResId, warning.title)
|
||||
tvMessage.text = getString(
|
||||
resId = warning.messageResId, default = warning.message, formatArgs = warning.messageFormatArg
|
||||
resId = warning.messageResId,
|
||||
default = warning.message,
|
||||
formatArgs = warning.messageFormatArg
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -73,17 +78,34 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
|
|||
WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> {
|
||||
binding.groupControlsTemporary.hide()
|
||||
binding.groupControlsRating.hide()
|
||||
binding.btnClose.hide()
|
||||
}
|
||||
WarningMessage.Type.Temporary -> {
|
||||
binding.groupControlsRating.hide()
|
||||
binding.groupControlsTemporary.show()
|
||||
binding.btnClose.hide()
|
||||
|
||||
val buttonAction =
|
||||
when (warning.titleResId) {
|
||||
R.string.warning_important_security_info -> {
|
||||
when {
|
||||
warning.titleResId == R.string.warning_important_security_info -> {
|
||||
View.OnClickListener {
|
||||
store.dispatch(WalletAction.ShowDialog.SignedHashesMultiWalletDialog)
|
||||
}
|
||||
}
|
||||
warning.messageResId == R.string.alert_funds_restoration_message -> {
|
||||
binding.btnClose.show()
|
||||
binding.btnClose.setOnClickListener {
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
store.dispatch(WalletAction.Warnings.RestoreFundsWarningClosed)
|
||||
}
|
||||
val locale = ConfigurationCompat
|
||||
.getLocales(Resources.getSystem().configuration)
|
||||
.get(0)
|
||||
val url = WarningMessagesManager.getRestoreFundsGuideUrl(locale.language)
|
||||
View.OnClickListener {
|
||||
store.dispatchOpenUrl(url)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
View.OnClickListener {
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
|
|
@ -99,6 +121,7 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
|
|||
WarningMessage.Type.AppRating -> {
|
||||
binding.groupControlsTemporary.hide()
|
||||
binding.groupControlsRating.show()
|
||||
binding.btnClose.show()
|
||||
val analyticsHandler = store.state.globalState.analyticsHandlers
|
||||
binding.btnClose.setOnClickListener {
|
||||
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.tap.features.wallet.ui.test
|
||||
|
||||
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.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TestWalletDetails {
|
||||
companion object {
|
||||
fun solanaRentExemptWarning(): List<TestAction> {
|
||||
val checker = SolanaRentWarningActionEmitter()
|
||||
return listOf(
|
||||
"BALANCE = 0.0" to { checker.setZeroBalance() },
|
||||
"BALANCE < 0.00089088" to { checker.setLessThanRentExempt() },
|
||||
"BALANCE > 0.00089088" to { checker.setMoreThanRentExempt() },
|
||||
"BALANCE = 0.00089087" to { checker.setLessThanRentExemptByOne() },
|
||||
"BALANCE = 0.00089088 (rent exempt)" to { checker.setForRentExemptBarrier() },
|
||||
"BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SolanaRentWarningActionEmitter {
|
||||
|
||||
private val zero = BigDecimal.ZERO
|
||||
private val one = BigDecimal(0.00000001)
|
||||
private val rentExemptBarrier = BigDecimal(0.00089088)
|
||||
private val lessThanRentExemptByOne = rentExemptBarrier.minus(one)
|
||||
private val moreThanRentExemptByOne = rentExemptBarrier.plus(one)
|
||||
private val moreThanRentExempt = rentExemptBarrier.plus(Random.nextDouble().toBigDecimal())
|
||||
private val lessThanRentExempt = rentExemptBarrier
|
||||
.minus(Random.nextDouble(0.0, rentExemptBarrier.minus(one).toDouble()).toBigDecimal())
|
||||
|
||||
fun setZeroBalance() {
|
||||
setBalance(zero)
|
||||
}
|
||||
|
||||
fun setForRentExemptBarrier() {
|
||||
setBalance(rentExemptBarrier)
|
||||
}
|
||||
|
||||
fun setLessThanRentExemptByOne() {
|
||||
setBalance(lessThanRentExemptByOne)
|
||||
}
|
||||
|
||||
fun setMoreThanRentExemptByOne() {
|
||||
setBalance(moreThanRentExemptByOne)
|
||||
}
|
||||
|
||||
fun setMoreThanRentExempt() {
|
||||
setBalance(moreThanRentExempt)
|
||||
}
|
||||
|
||||
fun setLessThanRentExempt() {
|
||||
setBalance(lessThanRentExempt)
|
||||
}
|
||||
|
||||
private fun setBalance(value: BigDecimal) {
|
||||
val amount = Amount(getBlockchainNetwork().blockchain).copy(value = value)
|
||||
getWalletManager().apply {
|
||||
TestActions.testAmountInjectionForWalletManagerEnabled = true
|
||||
wallet.setAmount(amount)
|
||||
}
|
||||
store.dispatch(WalletAction.LoadData)
|
||||
}
|
||||
|
||||
private fun getWalletManager(): WalletManager {
|
||||
return store.state.walletState.getWalletManager(getBlockchainNetwork())!!
|
||||
}
|
||||
|
||||
private fun getBlockchainNetwork(): BlockchainNetwork {
|
||||
val currency = store.state.walletState.getSelectedWalletData()!!.currency
|
||||
return BlockchainNetwork(currency.blockchain, currency.derivationPath, listOf())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ 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()) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun save(currencies: List<CurrenciesResponse.Currency>) {
|
||||
val json: String = converter.toJson(currencies)
|
||||
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
|
||||
}
|
||||
|
||||
fun restore(): List<CurrenciesResponse.Currency> {
|
||||
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
|
||||
val type = converter.typedList(CurrenciesResponse.Currency::class.java)
|
||||
if (json.isNullOrBlank()) return emptyList()
|
||||
|
||||
return converter.fromJson(json, type) ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,9 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
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 com.tangem.tap.network.coinmarketcap.FiatCurrency
|
||||
import java.util.*
|
||||
|
||||
|
||||
|
|
@ -20,39 +16,24 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
|
||||
val appRatingLaunchObserver: AppRatingLaunchObserver
|
||||
val usedCardsPrefStorage: UsedCardsPrefStorage
|
||||
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
|
||||
|
||||
init {
|
||||
incrementLaunchCounter()
|
||||
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
|
||||
usedCardsPrefStorage = UsedCardsPrefStorage(preferences)
|
||||
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
|
||||
usedCardsPrefStorage.migrate()
|
||||
}
|
||||
|
||||
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
val type = Types.newParameterizedType(List::class.java, FiatCurrency::class.java)
|
||||
moshi.adapter(type)
|
||||
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
|
||||
fiatCurrenciesPrefStorage.migrate()
|
||||
}
|
||||
|
||||
fun getAppCurrency(): FiatCurrencyName {
|
||||
return preferences.getString(APP_CURRENCY_KEY, DEFAULT_FIAT_CURRENCY)
|
||||
?: DEFAULT_FIAT_CURRENCY
|
||||
?: DEFAULT_FIAT_CURRENCY
|
||||
}
|
||||
|
||||
fun saveAppCurrency(fiatCurrencyName: FiatCurrencyName) {
|
||||
return preferences.edit().putString(APP_CURRENCY_KEY, fiatCurrencyName).apply()
|
||||
}
|
||||
|
||||
fun getFiatCurrencies(): List<FiatCurrency>? {
|
||||
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
|
||||
return if (json.isNullOrBlank()) null else fiatCurrenciesAdapter.fromJson(json) as List<FiatCurrency>
|
||||
}
|
||||
|
||||
fun saveFiatCurrencies(currencies: List<FiatCurrency>) {
|
||||
val json: String = fiatCurrenciesAdapter.toJson(currencies)
|
||||
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
|
||||
preferences.edit { putString(APP_CURRENCY_KEY, fiatCurrencyName) }
|
||||
}
|
||||
|
||||
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
|
||||
|
|
@ -63,7 +44,7 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
}
|
||||
|
||||
fun saveDisclaimerAccepted() {
|
||||
preferences.edit().putBoolean(DISCLAIMER_ACCEPTED_KEY, true).apply()
|
||||
preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) }
|
||||
}
|
||||
|
||||
fun wasDisclaimerAccepted(): Boolean {
|
||||
|
|
@ -71,7 +52,7 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
}
|
||||
|
||||
fun saveTwinsOnboardingShown() {
|
||||
preferences.edit().putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true).apply()
|
||||
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
|
||||
}
|
||||
|
||||
fun wasTwinsOnboardingShown(): Boolean {
|
||||
|
|
@ -83,13 +64,21 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
|
||||
}
|
||||
|
||||
fun wasRestoreFundsWarningClosed(): Boolean {
|
||||
return preferences.getBoolean(RESTORE_FUNDS_CLOSED_KEY, false)
|
||||
}
|
||||
|
||||
fun saveRestoreFundsWarningClosed() {
|
||||
preferences.edit { putBoolean(RESTORE_FUNDS_CLOSED_KEY, true) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES_NAME = "tapPrefs"
|
||||
private const val APP_CURRENCY_KEY = "appCurrency"
|
||||
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
|
||||
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
|
||||
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
|
||||
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
|
||||
private const val RESTORE_FUNDS_CLOSED_KEY = "restoreFundsClosed"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@ import timber.log.Timber
|
|||
*/
|
||||
class UsedCardsPrefStorage(
|
||||
private val preferences: SharedPreferences,
|
||||
private val jsonConverter: MoshiJsonConverter
|
||||
) {
|
||||
|
||||
private val jsonConverter = MoshiJsonConverter.INSTANCE
|
||||
|
||||
internal fun migrate() {
|
||||
val scannedIds = preferences.getString(SCANNED_CARDS_IDS_KEY, null) ?: return
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ class UsedCardsPrefStorage(
|
|||
fun scanned(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
|
||||
?: UsedCardInfo(cardId, true)
|
||||
?: UsedCardInfo(cardId, true)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
|
@ -39,7 +38,7 @@ class UsedCardsPrefStorage(
|
|||
fun activationStarted(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
|
||||
?: UsedCardInfo(cardId, isActivationStarted = true)
|
||||
?: UsedCardInfo(cardId, isActivationStarted = true)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
|
@ -55,7 +54,7 @@ class UsedCardsPrefStorage(
|
|||
fun activationFinished(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = false)
|
||||
?: UsedCardInfo(cardId, isActivationStarted = false)
|
||||
?: UsedCardInfo(cardId, isActivationStarted = false)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
|
|
|||
10
app/src/main/res/drawable/ic_clear.xml
Normal file
10
app/src/main/res/drawable/ic_clear.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#1C1C1E"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
||||
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="16"
|
||||
android:viewportHeight="19">
|
||||
<path
|
||||
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z"
|
||||
android:fillColor="#1C1C1E"/>
|
||||
android:fillColor="#1C1C1E"
|
||||
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -122,8 +122,12 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:textColor="@color/darkGray2"
|
||||
android:textSize="14sp"
|
||||
android:background="@drawable/shape_rectangle_rounded_4"
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="btn_close, btn_can_be_better, btn_really_cool" />
|
||||
app:constraint_referenced_ids="btn_can_be_better, btn_really_cool" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@
|
|||
<string name="token_icon">Значок токена</string>
|
||||
<string name="add_custom_token_title" translatable="false">Добавить токен</string>
|
||||
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
|
||||
<string name="custom_token_creation_error_required_field">Обязательное поле</string>
|
||||
<string name="custom_token_creation_error_network_not_selected">Пожалуйста, выберите сеть</string>
|
||||
<string name="custom_token_creation_error_empty_fields">Пожалуйста, заполните все поля</string>
|
||||
<string name="custom_token_creation_error_wrong_decimals">Количество знаков после запятой должно быть корректным числом не больше %d</string>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства.</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
<string name="alert_funds_restoration_message">Если вы совершили ошибку с выбором сети при переводе средств с биржи, эта инструкция поможет вам восстановить средства</string>
|
||||
|
||||
<string name="common_custom">Пользовательский</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,23 @@
|
|||
|
||||
<color name="colorPrimary">#0029FF</color>
|
||||
<color name="colorSecondary">#0022D4</color>
|
||||
<color name="accent">#1ACE80</color>
|
||||
<color name="accent">#19C878</color>
|
||||
<color name="accent_disabled">#801ACE80</color>
|
||||
|
||||
<color name="toolbarColor">@color/backgroundLightGray</color>
|
||||
<color name="menu_accent_color">@color/accent</color>
|
||||
|
||||
<color name="tapButtonColor">@color/accent</color>
|
||||
<color name="tapButtonDisabled">#661ACE80</color>
|
||||
<color name="tapButtonDisabled">@color/accent_disabled</color>
|
||||
<color name="tapButtonColorBlack">@color/darkGray6</color>
|
||||
<color name="tapButtonBlackDisabled">#6A6A6A</color>
|
||||
|
||||
<color name="success">#5AC461</color>
|
||||
<color name="success">@color/accent</color>
|
||||
<color name="error">#C4291C</color>
|
||||
<color name="warning">#FFB71B</color>
|
||||
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="toolbarColor">@color/backgroundLightGray</color>
|
||||
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="lightGray0">#F3F3F3</color>
|
||||
<color name="lightGray1">#F8F8FB</color>
|
||||
<color name="lightGray2">#DADADF</color>
|
||||
|
|
@ -62,6 +64,4 @@
|
|||
<color name="warning_critical">#CA0F03</color>
|
||||
|
||||
<color name="gray_button">#EFEFF0</color>
|
||||
|
||||
<color name="menu_accent_color">#18C077</color>
|
||||
</resources>
|
||||
|
|
@ -40,6 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
|
||||
<string name="add_custom_token_title" translatable="false">Add Token</string>
|
||||
<string name="custom_token_contract_address_input_title" translatable="false">Contract address</string>
|
||||
<string name="custom_token_creation_error_required_field" translatable="false">Required field</string>
|
||||
<string name="custom_token_creation_error_network_not_selected" translatable="false">Please select the network</string>
|
||||
<string name="custom_token_creation_error_empty_fields" translatable="false">Please fill in all the fields</string>
|
||||
<string name="custom_token_creation_error_wrong_decimals" translatable="false">Decimal number must be a valid integer, no higher than %d</string>
|
||||
|
|
|
|||
1
common/.gitignore
vendored
Normal file
1
common/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
9
common/build.gradle
Normal file
9
common/build.gradle
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
8
common/src/main/java/com/tangem/common/Validator.kt
Normal file
8
common/src/main/java/com/tangem/common/Validator.kt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.common
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface Validator<Data, Error> {
|
||||
fun validate(data: Data? = null): Error?
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue