Updated on 2026-08-14

This commit is contained in:
Tangem 2022-06-24 20:36:06 +04:00
commit ace7d2b050
179 changed files with 4455 additions and 3221 deletions

View file

@ -3,11 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
ext.configVar = [
configEnvironment: "CONFIG_ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
]
apply from: '../dependencies.gradle'
android {
compileSdkVersion 31
@ -19,7 +15,8 @@ android {
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
buildFeatures {
compose true
@ -31,8 +28,9 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'String', environmentConfig.environment, '\"prod\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
debug {
debuggable true
@ -42,16 +40,18 @@ android {
firebaseCrashlytics {
mappingFileUploadEnabled false
}
buildConfigField 'String', configVar.configEnvironment, '\"dev\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'true'
buildConfigField 'String', environmentConfig.environment, '\"dev\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'true'
}
debug_beta {
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'String', environmentConfig.environment, '\"prod\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
}
@ -93,9 +93,10 @@ dependencies {
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-77'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
implementation 'com.tangem:blockchain:develop-90'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154'
// WebView
implementation "androidx.browser:browser:1.3.0"
@ -117,8 +118,9 @@ dependencies {
// Camera
implementation 'com.otaliastudios:cameraview:2.6.3'
// Image Loading - Picasso
implementation 'com.squareup.picasso:picasso:2.71828'
// Image Loading
implementation 'io.coil-kt:coil:2.1.0'
implementation 'io.coil-kt:coil-compose:2.1.0'
// Firebase
implementation platform('com.google.firebase:firebase-bom:26.0.0')
@ -169,8 +171,7 @@ dependencies {
implementation 'androidx.compose.ui:ui-tooling:1.1.1'
implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0"
implementation("io.coil-kt:coil:2.0.0-rc01")
implementation("io.coil-kt:coil-compose:2.0.0-rc01")
implementation "com.github.skydoves:androidveil:1.1.2"
implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
@ -179,4 +180,4 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
}

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,18 @@
package com.tangem.tap
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.appsflyer.AppsFlyerLib
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@ -42,10 +45,11 @@ lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
class TapApplication : Application() {
class TapApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
DomainLayer.init()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
@ -59,8 +63,9 @@ class TapApplication : Application() {
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
currenciesRepository = CurrenciesRepository(this)
currenciesRepository = CurrenciesRepository(
this, store.state.domainNetworks.tangemTechService
)
walletConnectRepository = WalletConnectRepository(this)
initFeedbackManager()
@ -71,6 +76,10 @@ class TapApplication : Application() {
initAppsFlyer()
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(context = this)
}
private fun loadConfigs() {
val moshi = MoshiConverter.defaultMoshi()
val localLoader = FeaturesLocalLoader(this, moshi)
@ -104,11 +113,6 @@ class TapApplication : Application() {
}
data class LogConfig(
// disables both [internal, http]
val picasso: Boolean = BuildConfig.DEBUG,
val picassoInternal: Boolean = true,
val picassoHttp: Boolean = true,
val coil: Boolean = BuildConfig.DEBUG,
val storeAction: Boolean = BuildConfig.DEBUG,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,11 +8,13 @@ import com.tangem.wallet.R
@DrawableRes
fun Blockchain.getRoundIconRes(): Int {
return when (this) {
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> R.drawable.ic_arbitrum_round
Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet,-> R.drawable.ic_bitcoin_round
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_round
Blockchain.Litecoin -> R.drawable.ic_litecoin_round
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth_round
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> R.drawable.ic_eth_round
Blockchain.RSK -> R.drawable.ic_rsk_round
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_round
Blockchain.Tezos -> R.drawable.ic_tezos_round
@ -24,6 +26,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_round
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round
Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_round
else -> R.drawable.ic_tangem_logo
}
}
@ -31,11 +34,13 @@ fun Blockchain.getRoundIconRes(): Int {
@DrawableRes
fun Blockchain.getGreyedOutIconRes(): Int {
return when (this) {
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> R.drawable.ic_arbitrum_no_color
// Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet,-> R.drawable.ic_bitcoin_no_color
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_no_color
Blockchain.Litecoin -> R.drawable.ic_litecoin_no_color
Blockchain.Ethereum, Blockchain.EthereumTestnet, -> R.drawable.ic_eth_no_color
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet, -> R.drawable.ic_eth_no_color
Blockchain.RSK -> R.drawable.ic_rsk_no_color
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_no_color
Blockchain.Tezos -> R.drawable.ic_tezos_no_color
@ -47,6 +52,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color
Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_no_color
else -> R.drawable.ic_tangem_logo
}
}
@ -56,6 +62,7 @@ fun Blockchain.getNetworkName(): String {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"
Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20"
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
Blockchain.Tron, Blockchain.TronTestnet -> "TRC20"
else -> ""
}
}

View file

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

View file

@ -1,135 +0,0 @@
package com.tangem.tap.common.extensions
import android.graphics.*
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import com.squareup.picasso.Transformation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.wallet.R
fun Picasso.loadCurrenciesIcon(
imageView: ImageFilterView,
textView: TextView,
token: Token? = null,
blockchain: Blockchain,
) {
val url: String? = if (token != null) {
token.id?.let { getIconUrl(it) } ?: token.getCustomIconUrl()
?: IconsUtil.getTokenIconUri(blockchain, token)?.toString()
} else {
getIconUrl(blockchain.toNetworkId())
}
imageView.setImageDrawable(null)
imageView.colorFilter = null
textView.text = null
when {
token?.symbol == QCX -> {
this.load(R.drawable.ic_qcx)?.into(imageView)
}
token?.symbol == VOYR -> {
this.load(R.drawable.ic_voyr)?.into(imageView)
}
url != null -> {
if (token != null) {
setTokenImage(imageView, textView, token, blockchain)
}
this.load(url)
.transform(RoundedCornersTransform())
.noPlaceholder()
?.into(imageView,
object : Callback {
override fun onError(e: Exception?) {
setOfflineCurrencyImage(imageView, textView, token, blockchain)
}
override fun onSuccess() {
if (token != null) {
imageView.colorFilter = null
textView.text = null
}
if (blockchain.isTestnet()) imageView.saturation = 0f
}
})
}
else -> {
setOfflineCurrencyImage(imageView, textView, token, blockchain)
}
}
}
private const val QCX = "QCX"
private const val VOYR = "VOYRME"
private fun setOfflineCurrencyImage(
imageView: ImageFilterView,
textView: TextView,
token: Token?,
blockchain: Blockchain,
) {
when (token) {
null -> setBlockchainImage(imageView, textView, blockchain)
else -> setTokenImage(imageView, textView, token, blockchain)
}
}
private fun setBlockchainImage(
imageView: ImageFilterView,
textView: TextView,
blockchain: Blockchain,
) {
imageView.setImageResource(blockchain.getRoundIconRes())
imageView.colorFilter = null
if (blockchain.isTestnet()) imageView.saturation = 0f
textView.text = null
}
private fun setTokenImage(
imageView: ImageFilterView,
textView: TextView,
token: Token,
tokenBlockchain: Blockchain
) {
imageView.setImageResource(R.drawable.shape_circle)
if (tokenBlockchain.isTestnet()) {
imageView.saturation = 0f
} else {
imageView.setColorFilter(token.getColor())
}
textView.text = token.symbol.take(1)
}
private class RoundedCornersTransform : Transformation {
override fun transform(source: Bitmap): Bitmap {
val size = source.width.coerceAtMost(source.height)
val x = (source.width - size) / 2
val y = (source.height - size) / 2
val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
if (squaredBitmap != source) source.recycle()
val paint = Paint().apply {
shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
isAntiAlias = true
}
val rectF = RectF(0f, 0f, source.width.toFloat(), source.height.toFloat())
val radius = size / 8f
val bitmap = Bitmap.createBitmap(size, size, source.config)
Canvas(bitmap).drawRoundRect(rectF, radius, radius, paint)
squaredBitmap.recycle()
return bitmap
}
override fun key(): String = "rounded_corners"
}

View file

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

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.extensions
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.core.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
@ -9,8 +11,13 @@ import com.tangem.wallet.R
fun Token.getColor(): Int {
return try {
("#" + this.contractAddress.subSequence(2..7).toString())
.toColorInt()
.toColorInt()
} catch (exception: Exception) {
R.color.lightGray4
}
}
@ColorInt
fun Token.getTextColor(): Int {
return if (this.getColor().luminance > 0.5) Color.BLACK else Color.WHITE
}

View file

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

View file

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

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.images
import android.content.Context
import android.util.Log
import coil.ImageLoader
import coil.util.Logger
import com.tangem.tap.logConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
private const val COIL_LOG_TAG = "COIL"
fun createCoilImageLoader(
context: Context
): ImageLoader {
return ImageLoader.Builder(context)
.apply {
if (!logConfig.coil) return@apply
logger(CoilTimberLogger())
okHttpClient {
OkHttpClient.Builder()
.addNetworkInterceptor(
HttpLoggingInterceptor { message ->
Timber.tag(COIL_LOG_TAG).d(message)
}
.apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.build()
}
}
.build()
}
private class CoilTimberLogger : Logger {
override var level: Int = Log.DEBUG
override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) {
with(Timber.tag(COIL_LOG_TAG)) {
throwable?.let { e -> e(e, message) }
message?.let { msg -> d(msg) }
}
}
}

View file

@ -1,68 +0,0 @@
package com.tangem.tap.common.images
import android.app.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import com.tangem.tap.logConfig
import com.tangem.wallet.BuildConfig
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
class PicassoHelper {
companion object {
private const val KEEP_CACHE_MAX_DAYS = 7
fun initPicassoWithCaching(application: Application) {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = logIsEnabled()
picasso.setIndicatorsEnabled(BuildConfig.DEBUG)
Picasso.setSingletonInstance(picasso)
}
private fun getOkHttpForPicasso(application: Application): OkHttpClient {
return OkHttpClient.Builder().apply {
cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
callTimeout(15000, TimeUnit.MILLISECONDS)
if (logIsEnabled()) {
addDebugInterceptors(this)
}
addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxStale(KEEP_CACHE_MAX_DAYS, TimeUnit.DAYS)
.build()
val origRequest = chain.request()
val neverExpireRequest = origRequest.newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(neverExpireRequest)
}
}.build()
}
private fun addDebugInterceptors(okHttpBuilder: OkHttpClient.Builder) {
val picassoInterceptor = HttpLoggingInterceptor(PicassoOkHttpLogger()).apply {
level = HttpLoggingInterceptor.Level.BODY
}
okHttpBuilder.addInterceptor(picassoInterceptor)
}
private fun logIsEnabled(): Boolean = BuildConfig.DEBUG && logConfig.picasso
}
}
private class PicassoOkHttpLogger : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.d(message)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,49 +0,0 @@
package com.tangem.tap.domain
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import com.tangem.common.services.Result
/**
[REDACTED_AUTHOR]
*/
class UrlBitmapLoader {
private val mainHandler = Handler(Looper.getMainLooper())
fun loadBitmap(url: String, callback: (Result<Bitmap>) -> Unit) {
val target = DownloadTarget(callback)
protectedFromGarbageCollectorTargets.add(target)
mainHandler.post { Picasso.get().load(url).into(target) }
}
fun loadBitmap(url: String, target: DownloadTarget) {
protectedFromGarbageCollectorTargets.add(target)
Picasso.get().load(url).into(target)
}
}
private val protectedFromGarbageCollectorTargets = mutableListOf<Target>()
// It adds the ability to trigger multiple downloads with a unique callback.
open class DownloadTarget(
val callback: (Result<Bitmap>) -> Unit,
) : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
callback(Result.Success(bitmap))
protectedFromGarbageCollectorTargets.remove(this)
}
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
callback.invoke(Result.Failure(e ?: Exception("Unknown exception")))
protectedFromGarbageCollectorTargets.remove(this)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
}
}

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Blockchain.Arbitrum
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeStatus
@ -19,6 +20,7 @@ fun CurrencyExchangeManager.sellIsAllowed(currency: Currency): Boolean {
fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (currency.blockchain == Arbitrum) return false
if (!isBuyAllowed) return false
//TODO: temporary, for the 3.32 release, unlock all buy button

View file

@ -10,7 +10,7 @@ import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.Currency
fun WalletManagerFactory.makeWalletManagerForApp(

View file

@ -112,31 +112,44 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
this.card = card
val curves = card.getCurvesForNonCreatedWallets()
if (curves.isEmpty()) {
val createWalletResponses = card.wallets.map { CreateWalletResponse(card.cardId, it) }
proceedWithCreatedWallets(createWalletResponses, session, callback)
return
}
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val createWalletResponses = result.data.createWalletResponses
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponses, session, callback)
}
else -> {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(card = session.environment.card!!)
)
)
}
}
proceedWithCreatedWallets(result.data.createWalletResponses, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun proceedWithCreatedWallets(
createWalletResponses: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponses, session, callback)
}
else -> {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(card = session.environment.card!!)
)
)
}
}
}
private fun linkPrimaryCard(
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,56 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateHashCode
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@JsonClass(generateAdapter = true)
data class BlockchainNetwork(
val blockchain: Blockchain,
val derivationPath: String?,
val tokens: List<Token>
) {
constructor(blockchain: Blockchain, card: Card) : this(
blockchain = blockchain,
derivationPath = if (card.settings.isHDWalletAllowed) blockchain.derivationPath(card.derivationStyle)?.rawPath else null,
tokens = emptyList()
)
fun updateTokens(tokens: List<Token>): BlockchainNetwork {
return copy(
tokens = (this.tokens + tokens).distinct()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BlockchainNetwork
if (blockchain != other.blockchain) return false
if (derivationPath != other.derivationPath) return false
return true
}
override fun hashCode(): Int = calculateHashCode(
blockchain.hashCode(), derivationPath?.hashCode() ?: 0
)
companion object {
fun fromWalletManager(walletManager: WalletManager): BlockchainNetwork {
return BlockchainNetwork(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
walletManager.cardTokens.toList()
)
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
@JsonClass(generateAdapter = true)
data class ObsoleteTokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
val customIconUrl: String?,
) {
fun toTokenDao(blockchain: Blockchain): TokenDao {
return TokenDao(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimalCount = decimalCount,
blockchainDao = BlockchainDao.fromBlockchain(blockchain),
customIconUrl = customIconUrl
)
}
}

View file

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

View file

@ -63,11 +63,17 @@ class WriteProtectedIssuerDataTask(
val counter = (readResponse.issuerDataCounter ?: 0) + 1
val data = twinPublicKey + cardSignature
val signedByIssuer = FileHashHelper.prepareHashes(
cardId, data, counter, issuerKeys.privateKey
cardId = cardId,
fileData = data,
fileCounter = counter,
fileName = null,
privateKey = issuerKeys.privateKey,
)
WriteIssuerDataCommand(
data, signedByIssuer.finalizingSignature!!,
counter, issuerKeys.publicKey
issuerData = data,
issuerDataSignature = signedByIssuer.finalizingSignature!!,
issuerDataCounter = counter,
issuerPublicKey = issuerKeys.publicKey,
).run(session, callback)
}
}

View file

@ -5,11 +5,15 @@ import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
import timber.log.Timber
import java.nio.charset.Charset
class WalletConnectRepository(val context: Application) {
private val moshi = MoshiConverter.defaultMoshi()
@ -35,17 +39,29 @@ class WalletConnectRepository(val context: Application) {
fun loadSavedSessions(): List<WalletConnectSession> {
return try {
val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS)
.hexToUtf8()
walletConnectAdapter.fromJson(json)!!.map { it.toSession() }
} catch (exception: Exception) {
Timber.e(exception)
emptyList()
}
}
private fun saveSessions(sessions: List<WalletConnectSession>) {
val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) })
.utf8ToHex() // convert to hex to solve problems with saving text with emojis
Timber.e("WC sessions, saving following json: $json")
context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS)
}
private fun String.utf8ToHex(): String {
return this.toByteArray().toHexString()
}
private fun String.hexToUtf8(): String {
return this.hexToBytes().toString(Charset.defaultCharset())
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()

View file

@ -4,11 +4,9 @@ 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.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.wallet.R
import org.rekotlin.Action
@ -19,9 +17,6 @@ sealed class DetailsAction : Action {
val scanResponse: ScanResponse,
val wallets: List<Wallet>,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
val tangemTechService: TangemTechService,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -47,13 +42,6 @@ sealed class DetailsAction : Action {
object CreateBackup : DetailsAction()
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<CurrenciesResponse.Currency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()
}
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()
@ -71,4 +59,5 @@ sealed class DetailsAction : Action {
}
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
}

View file

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

View file

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

View file

@ -3,10 +3,8 @@ 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.common.entities.FiatCurrency
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.store
import org.rekotlin.StateType
@ -17,12 +15,12 @@ data class DetailsState(
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val cardTermsOfUseUrl: Uri? = null,
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -50,10 +48,4 @@ data class SecurityScreenState(
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<CurrenciesResponse.Currency>? = null,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -16,15 +16,17 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -257,14 +259,16 @@ class WalletConnectMiddleware {
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {
currenciesRepository.saveUpdatedCurrency(
card.cardId,
BlockchainNetwork.fromWalletManager(walletManager)
)
scope.launch {
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {
currenciesRepository.saveUpdatedCurrency(
card.cardId,
BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
}
return walletManager

View file

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

View file

@ -17,6 +17,7 @@ import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsBinding
@ -24,7 +25,6 @@ import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private var currencySelectionDialog = CurrencySelectionDialog()
private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
@ -115,11 +115,14 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(DetailsAction.CreateBackup)
}
tvAppCurrency.text = state.appCurrencyState.fiatCurrencyName
tvAppCurrency.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
tvAppCurrencyTitle.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
tvAppCurrency.text = state.appCurrency.code
tvAppCurrencyTitle.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
tvAppCurrency.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
@ -140,17 +143,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
null -> null
}
currentSecurity?.let { tvSecurity.text = getString(it) }
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
currencySelectionDialog.show(
state.appCurrencyState.fiatCurrencies,
state.appCurrencyState.fiatCurrencyName,
requireContext()
)
} else {
currencySelectionDialog.clear()
}
}
}

View file

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

View file

@ -10,7 +10,7 @@ import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.extensions.fadeIn
import com.tangem.tangem_sdk_new.extensions.fadeOut
@ -67,11 +67,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
override fun newState(state: OnboardingNoteState) {
if (activity == null || view == null) return
Picasso.get()
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvFrontCard)
binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress

View file

@ -7,7 +7,7 @@ import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
@ -54,11 +54,11 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
if (activity == null || view == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
Picasso.get()
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvFrontCard)
binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress

View file

@ -9,13 +9,18 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
@ -75,24 +80,28 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactorForWelcomeState = typedValue.float
twinsWidget = TwinsCardWidget(LeapfrogWidget(binding.onboardingTopContainer.cardsContainer), deviceScaleFactorForWelcomeState) {
twinsWidget = TwinsCardWidget(
LeapfrogWidget(binding.onboardingTopContainer.cardsContainer),
deviceScaleFactorForWelcomeState
) {
285f * deviceScaleFactorForWelcomeState
}
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
btnRefreshBalanceWidget =
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvTwinFrontCard)
binding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.error(R.drawable.card_placeholder_white)
.placeholder(R.drawable.card_placeholder_white)
?.into(binding.onboardingTopContainer.imvTwinBackCard)
binding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) {
placeholder(R.drawable.card_placeholder_white)
error(R.drawable.card_placeholder_white)
fallback(R.drawable.card_placeholder_white)
}
}
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =

View file

@ -11,9 +11,9 @@ import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayoutMediator
import com.squareup.picasso.Picasso
import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
@ -22,7 +22,12 @@ import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -124,11 +129,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
private fun loadImageIntoImageView(url: String?, view: ImageView) {
Picasso.get()
.load(url)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(view)
view.load(url) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
private fun setupCreateWalletState() = with(binding) {

View file

@ -24,7 +24,7 @@ import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*

View file

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

View file

@ -7,6 +7,7 @@ import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.store
import java.math.BigDecimal
@ -15,10 +16,6 @@ import java.math.BigDecimal
*/
class ReceiptReducer : SendInternalReducer {
companion object {
const val EMPTY = "-"
}
private lateinit var sendState: SendState
private lateinit var amountState: AmountState
private lateinit var feeState: FeeState
@ -128,7 +125,8 @@ class ReceiptReducer : SendInternalReducer {
val totalFiat = amountFiat.plus(feeFiat)
ReceiptTokenFiat(
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true)
.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
@ -136,9 +134,9 @@ class ReceiptReducer : SendInternalReducer {
)
} else {
ReceiptTokenFiat(
amountFiat = EMPTY,
feeFiat = EMPTY,
totalFiat = EMPTY,
amountFiat = UNKNOWN_AMOUNT_SIGN,
feeFiat = UNKNOWN_AMOUNT_SIGN,
totalFiat = UNKNOWN_AMOUNT_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
@ -160,15 +158,15 @@ class ReceiptReducer : SendInternalReducer {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
totalFiat = EMPTY,
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = UNKNOWN_AMOUNT_SIGN,
symbols = symbols
)
}
@ -177,7 +175,7 @@ class ReceiptReducer : SendInternalReducer {
private fun determineSymbols(wallet: Wallet, amountType: AmountType): ReceiptSymbols {
return ReceiptSymbols(
fiat = store.state.globalState.appCurrency,
fiat = store.state.globalState.appCurrency.code,
crypto = wallet.blockchain.currency,
token = when (amountType) {
is AmountType.Token -> amountType.token.symbol
@ -205,7 +203,10 @@ class ReceiptReducer : SendInternalReducer {
return when {
!isToken && sendState.coinIsConvertible() -> sendState.coinConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
isToken && sendState.tokenIsConvertible() -> sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
else -> EMPTY
else -> UNKNOWN_AMOUNT_SIGN
}
}
private fun String.addPrecisionSign(): String =
("${feeState.feePrecision.symbol} $this").trim()
}

View file

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

View file

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

View file

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

View file

@ -17,12 +17,13 @@ import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.reducers.ReceiptReducer
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
import com.tangem.tap.features.wallet.redux.WalletState.Companion.ROUGH_SIGN
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.store
import com.tangem.wallet.R
@ -257,12 +258,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
val mainLayout = clReceiptContainer as ViewGroup
val totalLayout = llTotalContainer.llTotal as ViewGroup
val totalTokenLayout = llTotalContainer.flTotalTokenCrypto as ViewGroup
fun getString(id: Int, vararg formatStrings: String): String =
mainLayout.context.getString(id, *formatStrings)
val rough = getString(R.string.sign_rough)
fun roughOrEmpty(value: String): String =
if (value == ReceiptReducer.EMPTY) value else "$rough $value"
fun getString(id: Int, vararg formatStrings: String): String = mainLayout.getString(id, *formatStrings)
fun roughOrEmpty(value: String): String {
return if (value == UNKNOWN_AMOUNT_SIGN) value else "$ROUGH_SIGN $value"
}
when (state.visibleTypeOfReceipt) {
ReceiptLayoutType.FIAT -> {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,9 +17,13 @@ sealed class TokensAction : Action {
val supportedBlockchains: List<Blockchain>? = null,
val scanResponse: ScanResponse? = null
) : TokensAction() {
data class Success(val currencies: List<Currency>) : TokensAction()
data class Success(val currencies: List<Currency>, val loadMore: Boolean) : TokensAction()
object Failure : TokensAction()
}
data class LoadMore(val scanResponse: ScanResponse? = null
) : TokensAction()
data class SetAddedCurrencies(
val wallets: List<WalletData>, val derivationStyle: DerivationStyle?
) : TokensAction()
@ -32,4 +36,6 @@ sealed class TokensAction : Action {
) : TokensAction()
object PrepareAndNavigateToAddCustomToken : TokensAction()
data class SetSearchInput(val searchInput: String) : TokensAction()
}

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.ContractAddress
@ -46,6 +47,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
})
val inflater = TransitionInflater.from(requireContext())
@ -81,13 +83,21 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
store.dispatchNotification(R.string.contract_address_copied_message)
}
val onLoadMore = {
store.dispatch(
TokensAction.LoadMore(
scanResponse = store.state.globalState.scanResponse
)
)
}
cvCurrencies.setContent {
AppCompatTheme {
CurrenciesScreen(
tokensState = tokensState,
searchInput = searchInput,
onSaveChanges = onSaveChanges,
onNetworkItemClicked = onNetworkItemClicked
onNetworkItemClicked = onNetworkItemClicked,
onLoadMore = onLoadMore
)
}
}
@ -123,10 +133,11 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
searchView.queryHint = searchView.getString(R.string.add_token_search_hint)
searchView.maxWidth = android.R.attr.width
searchView.inputtedTextAsFlow()
.debounce(400)
.debounce(800)
.distinctUntilChanged()
.onEach {
searchInput.value = it.lowercase()
dispatchOnMain(TokensAction.SetSearchInput(searchInput.value))
}
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
package com.tangem.tap.features.wallet.models
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.features.wallet.redux.ProgressState
import java.math.BigDecimal
data class TotalBalance(
val state: ProgressState,
val fiatAmount: BigDecimal,
val fiatCurrency: FiatCurrency,
)

View file

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

View file

@ -1,17 +1,22 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.card.Card
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
import org.rekotlin.Action
sealed class WalletAction : Action {
@ -20,6 +25,7 @@ sealed class WalletAction : Action {
data class SetIfTestnetCard(val isTestnet: Boolean) : WalletAction()
object LoadData : WalletAction() {
object Refresh : WalletAction()
data class Failure(val error: TapError) : WalletAction()
}
@ -98,7 +104,10 @@ sealed class WalletAction : Action {
data class LoadFiatRate(
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
) : WalletAction() {
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
data class Success(
val fiatRates: Map<Currency, BigDecimal?>
) : WalletAction()
object Failure : WalletAction()
}
@ -113,10 +122,8 @@ sealed class WalletAction : Action {
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
object EmptyField : WalletAction(), ErrorAction {
override val error = TapError.PayIdEmptyField
}
@ -129,13 +136,18 @@ sealed class WalletAction : Action {
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object SignedHashesMultiWalletDialog : WalletAction()
object ChooseTradeActionDialog : WalletAction()
}
sealed class DialogAction : WalletAction() {
data class QrCode(
val currency: Currency,
val selectedAddress: AddressData,
) : DialogAction()
object HideDialog : WalletAction()
object SignedHashesMultiWalletDialog : DialogAction()
object ChooseTradeActionDialog : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
object Hide : DialogAction()
}
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
@ -157,10 +169,15 @@ sealed class WalletAction : Action {
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class SetWalletRent(
val blockchain: BlockchainNetwork,
val wallet: Wallet,
val minRent: String,
val rentExempt: String
) : WalletAction()
data class RemoveWalletRent(val blockchain: BlockchainNetwork) : WalletAction()
data class RemoveWalletRent(val wallet: Wallet) : WalletAction()
sealed class AppCurrencyAction : WalletAction() {
object ChooseAppCurrency : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
}
}

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.Currency

View file

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

View file

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

View file

@ -15,26 +15,35 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.models.getSendableAmounts
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import java.math.BigDecimal
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
@ -42,12 +51,20 @@ 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()
private val warningsMiddleware = WarningsMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
private val walletDialogMiddleware = WalletDialogsMiddleware()
private val appCurrencyMiddleware by lazy(mode = LazyThreadSafetyMode.NONE) {
AppCurrencyMiddleware(
tangemTechService = store.state.domainNetworks.tangemTechService,
tapWalletManager = store.state.globalState.tapWalletManager,
fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
@ -72,6 +89,8 @@ class WalletMiddleware {
walletState,
globalState
)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is WalletAction.DialogAction -> walletDialogMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
@ -104,7 +123,7 @@ class WalletMiddleware {
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.LoadFiatRate -> {
val appCurrencyId = globalState.appCurrency
val appCurrencyId = globalState.appCurrency.code
scope.launch {
val coinsList = when {
action.wallet != null -> {
@ -122,11 +141,15 @@ class WalletMiddleware {
)
when (ratesResult) {
is Result.Success -> {
ratesResult.data.loadedRates.forEach {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it.toPair()))
ratesResult.data.loadedRates.let {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it))
}
ratesResult.data.failedRates.forEach { (currency, throwable) ->
Timber.e(throwable, "Loading rates failed for [%s]", currency.currencySymbol)
Timber.e(
throwable,
"Loading rates failed for [%s]",
currency.currencySymbol
)
}
}
is Result.Failure -> {
@ -184,7 +207,8 @@ class WalletMiddleware {
store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded)
}
}
is WalletAction.LoadData -> {
is WalletAction.LoadData,
is WalletAction.LoadData.Refresh -> {
scope.launch {
val scanNoteResponse = globalState.scanResponse ?: return@launch
if (walletState.walletsData.isNotEmpty()) {
@ -219,15 +243,6 @@ class WalletMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData =
walletState.getWalletData(walletState.selectedCurrency) ?: return
val selectedAddressData =
selectedWalletData.walletAddresses?.selectedAddress ?: return
val currency = selectedWalletData.currency
store.dispatchDialogShow(AppDialog.AddressInfoDialog(currency, selectedAddressData))
}
}
}
@ -243,13 +258,13 @@ class WalletMiddleware {
PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletStore?.walletManager)
}
} else {
val amounts = walletStore?.walletManager?.wallet?.amounts?.toSendableAmounts()
val amounts = walletStore?.walletManager?.wallet?.getSendableAmounts()
if (currency != null && state.isMultiwalletAllowed) {
when (currency) {
is Currency.Blockchain -> {
val amountToSend =
amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData.fiatRate,
@ -259,7 +274,7 @@ class WalletMiddleware {
is Currency.Token -> {
val amountToSend =
amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.Send.ChooseCurrency(amounts)
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
prepareSendActionForToken(
amount = amountToSend,
state = state,
@ -322,7 +337,7 @@ class WalletMiddleware {
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val outgoingAmount = outgoingTxs.sumOf { it.amountValue ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
@ -330,18 +345,16 @@ class WalletMiddleware {
val currency = walletManager.wallet.blockchain.currency
if (show) {
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
wallet = walletManager.wallet,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
))
dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet))
}
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
}
}

View file

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

View file

@ -3,14 +3,15 @@ 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
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -34,8 +35,8 @@ class MultiWalletReducer {
}
val walletData = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
blockchain.blockchain.fullName,
status = BalanceStatus.Loading,
currency = blockchain.blockchain.fullName,
currencySymbol = blockchain.blockchain.currency,
token = cardToken
),
@ -70,8 +71,8 @@ class MultiWalletReducer {
val walletData = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
action.blockchain.blockchain.fullName,
status = BalanceStatus.Loading,
currency = action.blockchain.blockchain.fullName,
currencySymbol = action.blockchain.blockchain.currency,
),
walletAddresses = createAddressList(wallet),
@ -106,17 +107,17 @@ class MultiWalletReducer {
throw NullPointerException("MultiWallet.TokenLoaded: WalletManager must be no NULL")
}
val pendingTransactions = wallet.recentTransactions.toPendingTransactions(wallet.address)
val sendButtonEnabled =
action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val tokenPendingTransactions = pendingTransactions
.filter { it.currency == action.amount.currencySymbol }
val pendingTransactions = wallet.getPendingTransactions()
val tokenPendingTransactions = pendingTransactions.filterByToken(action.token)
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(currency)
val isTokenSendButtonEnabled = tokenWalletData?.shouldEnableTokenSendButton() == true
&& pendingTransactions.isEmpty()
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
@ -125,13 +126,12 @@ class MultiWalletReducer {
action.amount.decimals, action.amount.currencySymbol
),
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
action.amount.value
?.toFiatString(it, store.state.globalState.appCurrency)
},
action.amount.value?.toFiatString(it, store.state.globalState.appCurrency.symbol)
} ?: UNKNOWN_AMOUNT_SIGN,
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
currency = Currency.Token(
token = action.token,
blockchain = action.blockchain.blockchain,
@ -178,7 +178,7 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
return WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
status = BalanceStatus.Loading,
currency = this.name,
currencySymbol = this.symbol
),

View file

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

View file

@ -0,0 +1,60 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import java.math.BigDecimal
fun List<WalletData>.findProgressState(): ProgressState {
return this
.mapToProgressState()
.reduce(ProgressState::or)
}
fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
return this
.map { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
private fun List<WalletData>.mapToProgressState(): List<ProgressState> {
return this.map { wallet ->
if (wallet.fiatRate == null) ProgressState.Error
else when (wallet.currencyData.status) {
BalanceStatus.Refreshing -> ProgressState.Refreshing
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress,
BalanceStatus.NoAccount -> ProgressState.Done
BalanceStatus.Unreachable,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain -> ProgressState.Error
BalanceStatus.Loading,
null -> ProgressState.Loading
}
}
}
private infix fun ProgressState.or(newState: ProgressState): ProgressState {
return when (this) {
ProgressState.Loading -> when (newState) {
ProgressState.Loading,
ProgressState.Refreshing,
ProgressState.Error,
ProgressState.Done -> this
}
ProgressState.Done,
ProgressState.Error -> when (newState) {
ProgressState.Loading,
ProgressState.Refreshing,
ProgressState.Error -> newState
ProgressState.Done -> this
}
ProgressState.Refreshing -> when (newState) {
ProgressState.Loading -> this
ProgressState.Refreshing,
ProgressState.Error,
ProgressState.Done -> newState
}
}
}

View file

@ -3,24 +3,37 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.TradeCryptoState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletAddresses
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
import java.math.RoundingMode
import org.rekotlin.Action
class WalletReducer {
companion object {
@ -32,6 +45,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val multiWalletReducer = MultiWalletReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
val appCurrencyReducer = AppCurrencyReducer()
if (action !is WalletAction) return state.walletState
@ -70,9 +84,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallets = newState.wallets
.map {
it.copy(
walletsData = it.walletsData.map {
.map { store ->
store.copy(
walletsData = store.walletsData.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable
@ -110,6 +124,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
else -> { /* no-op */
}
}
}
@ -119,14 +135,25 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
error = null,
)
}
is WalletAction.LoadData.Refresh -> {
newState = newState.copy(
state = ProgressState.Refreshing,
error = null,
)
}
is WalletAction.LoadWallet -> {
val balanceStatus = if (newState.state == ProgressState.Refreshing) {
BalanceStatus.Refreshing
} else {
BalanceStatus.Loading
}
if (action.blockchain == null) {
val wallets = newState.wallets.map {
it.copy(
walletsData = it.walletsData.map { walletData ->
walletData.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Loading,
status = balanceStatus,
currency = walletData.currencyData.currency,
currencySymbol = walletData.currencyData.currencySymbol,
),
@ -146,17 +173,14 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
val currencies = listOf(Currency.fromBlockchainNetwork(action.blockchain)) +
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(
action.blockchain,
it
)
}
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(action.blockchain, it)
}
val newWallets = newState.walletsData.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = BalanceStatus.Loading,
status = balanceStatus,
currency = wallet.currencyData.currency,
currencySymbol = wallet.currencyData.currencySymbol,
),
@ -178,31 +202,37 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
walletState = newState
)
is WalletAction.LoadWallet.NoAccount -> {
val walletData = newState.getWalletData(action.blockchain)?.copy(
currencyData = BalanceWidgetData(
BalanceStatus.NoAccount, action.wallet.blockchain.fullName,
currencySymbol = action.wallet.blockchain.currency,
amountToCreateAccount = action.amountToCreateAccount
val amount = BigDecimal.ZERO
val fiatAmount = BigDecimal.ZERO
val walletData = newState.getWalletData(action.blockchain)?.let { walletData ->
val walletBlockchain = walletData.currency.blockchain
walletData.copy(
currencyData = BalanceWidgetData(
status = BalanceStatus.NoAccount,
currency = action.wallet.blockchain.fullName,
currencySymbol = action.wallet.blockchain.currency,
amount = amount,
amountFormatted = amount.toFormattedCurrencyString(
decimals = walletBlockchain.decimals(),
currency = walletBlockchain.currency,
),
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount.toFormattedFiatValue(
fiatCurrencyName = state.globalState.appCurrency.symbol
),
amountToCreateAccount = action.amountToCreateAccount,
)
)
)
}
var updatedWalletStore = newState.getWalletStore(action.blockchain)
?.updateWallets(listOfNotNull(walletData))
val progressState =
if (updatedWalletStore?.walletsData?.any { it.currencyData.status == BalanceStatus.Loading } == true) {
ProgressState.Loading
} else {
ProgressState.Done
}
updatedWalletStore =
updatedWalletStore?.updateWallets(
newState.updateTradeCryptoState(exchangeManager, updatedWalletStore.walletsData)
)
newState = newState.updateWalletStore(updatedWalletStore)
.copy(
state = progressState
)
}
is WalletAction.LoadWallet.Failure -> {
@ -261,7 +291,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
is WalletAction.LoadFiatRate.Success ->
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
newState = setNewFiatRate(action.fiatRates, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (state.twinCardsState.cardNumber) {
@ -271,21 +301,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowDialog.SignedHashesMultiWalletDialog -> {
newState = newState.copy(walletDialog = WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.ShowDialog.ChooseTradeActionDialog -> {
newState = newState.copy(walletDialog = WalletDialog.ChooseTradeActionDialog)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
is WalletAction.Send.ChooseCurrency -> {
newState = newState.copy(
walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts)
)
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.TradeCryptoAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val selectedWalletData = newState.getWalletData(newState.selectedCurrency)
@ -305,23 +320,20 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.SetWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData == null) {
newState
} else {
walletData =
walletData.copy(warningRent = WalletRent(action.minRent, action.rentExempt))
newState = newState.updateWalletsData(listOf(walletData))
}
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletRent = WalletRent(action.minRent, action.rentExempt)
val walletsData = walletStore.walletsData.map { it.copy(walletRent = walletRent) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.RemoveWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData == null) {
newState
} else {
walletData = walletData.copy(warningRent = null)
newState = newState.updateWalletsData(listOf(walletData))
}
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletsData = walletStore.walletsData.map { it.copy(walletRent = null) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}
else -> { /* no-op */
}
}
return newState
@ -376,56 +388,88 @@ private fun handleCheckSignedHashesActions(
private fun setNewFiatRate(
fiatRate: Pair<Currency, BigDecimal?>,
appCurrency: FiatCurrencyName, state: WalletState
fiatRates: Map<Currency, BigDecimal?>,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val rate = fiatRate.second ?: return state
val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency, RoundingMode.HALF_UP)
val currency = fiatRate.first
val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal ->
rate.toFiatRateString(
fiatCurrencyName = appCurrency.symbol
)
}
return if (!state.isMultiwalletAllowed) {
setSingeWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
return if (state.isMultiwalletAllowed) {
setMultiWalletFiatRate(
fiatRates = fiatRates.mapNotNullValues { it.value },
rateFormatter = rateFormatter,
appCurrency = appCurrency,
state = state
)
} else {
setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
val fiatRate = fiatRates.entries.firstOrNull()
val currency = fiatRate?.key ?: return state
val rate = fiatRate.value ?: return state
setSingleWalletFiatRate(
rate = rate,
rateFormatted = rateFormatter(rate),
currency = currency,
appCurrency = appCurrency,
state = state
)
}
}
private fun setMultiWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: Currency,
appCurrency: FiatCurrencyName, state: WalletState
fiatRates: Map<Currency, BigDecimal>,
rateFormatter: (BigDecimal) -> String,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val newWalletsData = fiatRates.mapNotNull { (currency, rate) ->
val walletStore = state.getWalletStore(currency) ?: return@mapNotNull null
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return@mapNotNull null
val currencyData = walletData.currencyData
val walletStore = state.getWalletStore(currency) ?: return state
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return state
var fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token ->
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
}
if (currencyData.status == BalanceStatus.NoAccount && fiatAmount == null) {
fiatAmount = BigDecimal.ZERO.setScale(2)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.symbol)
val fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token ->
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
walletData.copy(
currencyData = currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount
),
fiatRate = rate,
fiatRateString = rateFormatter(rate)
)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency)
val newWalletData = state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount
),
fiatRate = rate, fiatRateString = rateFormatted
)
return state.updateWalletData(newWalletData)
return state
.updateWalletsData(newWalletsData)
}
private fun setSingeWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: Currency,
appCurrency: FiatCurrencyName, state: WalletState
private fun setSingleWalletFiatRate(
rate: BigDecimal,
rateFormatted: String,
currency: Currency,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val wallet = state.primaryWalletManager?.wallet ?: return state
val token = wallet.getFirstToken()
if (currency == state.primaryWallet?.currency) {
val fiatAmount = wallet.amounts[AmountType.Coin]?.value
?.toFiatString(rate, appCurrency)
?.toFiatString(rate, appCurrency.code)
val walletData = state.primaryWallet.copy(
currencyData = state.primaryWallet.currencyData.copy(fiatAmountFormatted = fiatAmount),
fiatRate = rate,
@ -433,7 +477,9 @@ private fun setSingeWalletFiatRate(
)
return state.updateWalletData(walletData)
} else if (currency is Currency.Token && currency.token == token) {
val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency)
val tokenFiatAmount = wallet.getTokenAmount(token)
?.value
?.toFiatString(rate, appCurrency.code)
val tokenData = state.primaryWallet?.currencyData?.token?.copy(
fiatAmount = tokenFiatAmount,
fiatRate = rate,

View file

@ -13,6 +13,7 @@ enum class BalanceStatus {
SameCurrencyTransactionInProgress,
Unreachable,
Loading,
Refreshing,
NoAccount,
EmptyCard,
UnknownBlockchain
@ -25,8 +26,8 @@ data class BalanceWidgetData(
val blockchainAmount: BigDecimal? = BigDecimal.ZERO,
val amount: BigDecimal? = null,
val amountFormatted: String? = null,
val fiatAmountFormatted: String? = null,
val fiatAmount: BigDecimal? = null,
val fiatAmountFormatted: String? = null,
val token: TokenData? = null,
val amountToCreateAccount: String? = null,
val errorMessage: String? = null

View file

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

View file

@ -12,10 +12,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.tangem_sdk_new.extensions.dpToPx
import coil.load
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -24,8 +24,11 @@ import com.tangem.tap.domain.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
@ -79,7 +82,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(WalletAction.Scan)
}
setupWarningsRecyclerView()
walletView.changeWalletView(this, binding)
@ -100,7 +103,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
with(binding) {
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt()))
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
}
}
@ -132,15 +135,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
showWarningsIfPresent(state.mainWarningsList)
binding.srlWallet.isRefreshing = state.state == ProgressState.Refreshing
binding.srlWallet.setOnRefreshListener {
if (state.state != ProgressState.Loading) {
store.dispatch(WalletAction.LoadData)
if (state.state != ProgressState.Loading ||
state.state != ProgressState.Refreshing
) {
store.dispatch(WalletAction.LoadData.Refresh)
}
}
if (state.state != ProgressState.Loading) {
binding.srlWallet.isRefreshing = false
}
}
private fun showWarningsIfPresent(warnings: List<WarningMessage>) {
@ -163,11 +165,11 @@ 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)
binding.ivCard.load(cardImage?.artworkId) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -179,8 +181,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
CardTou(),
store.state.globalState.appCurrency,
tangemTechService = store.state.domainNetworks.tangemTechService
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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