Updated on 2026-08-14

This commit is contained in:
Tangem 2022-05-03 14:21:14 +03:00
commit b804a4e7a0
141 changed files with 3454 additions and 1711 deletions

View file

@ -4,6 +4,11 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
ext.configVar = [
configEnvironment: "CONFIG_ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
]
android {
compileSdkVersion 31
defaultConfig {
@ -13,6 +18,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
}
buildFeatures {
compose true
@ -24,7 +31,8 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
}
debug {
debuggable true
@ -34,15 +42,18 @@ android {
firebaseCrashlytics {
mappingFileUploadEnabled false
}
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"dev\"'
buildConfigField 'String', configVar.configEnvironment, '\"dev\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'true'
}
debug_beta {
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
@ -69,8 +80,8 @@ repositories {
dependencies {
implementation fileTree(include: ['*.aar'], dir: 'libs')
implementation implementation(project(path: ':domain'))
// TODO: refactoring: only for backwards compatibility with non-relocated services to the network module
implementation implementation(project(path: ':network'))
implementation implementation(project(path: ':common'))
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
@ -82,7 +93,7 @@ dependencies {
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-71'
implementation 'com.tangem:blockchain:develop-77'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'

View file

@ -1,13 +1,13 @@
{
"tokens" : [
"coins" : [
{
"id" : "polygon/test",
"id" : "matic-token",
"symbol" : "MATIC",
"name" : "Polygon",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0x0000000000000000000000000000000000001010",
"networkId" : "polygon-pos/test",
"contractAddress" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
}
]
@ -16,20 +16,15 @@
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"networkId" : "polygon-pos/test",
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
}
]
@ -38,10 +33,10 @@
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"networkId" : "polygon-pos/test",
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
}
]
@ -50,15 +45,15 @@
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"networkId" : "polygon-pos/test",
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
}
]
@ -67,10 +62,10 @@
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"networkId" : "polygon-pos/test",
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
}
]
@ -79,20 +74,15 @@
"id" : "Tether USD-USDT",
"symbol" : "USDT",
"name" : "Tether USD",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"networkId" : "polygon-pos/test",
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
},
{
"networkId" : "binance-smart-chain",
"address" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"address" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
}
]
@ -101,10 +91,10 @@
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"networkId" : "polygon-pos/test",
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
}
]
@ -113,30 +103,32 @@
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"contracts" : [
"networks" : [
{
"networkId" : "polygon-pos",
"address" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"networkId" : "polygon-pos/test",
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
}
]
},
{
"id" : "solana/test",
"id" : "solana",
"symbol" : "SOL",
"name" : "Solana",
"contracts" : [
"networks" : [
{
"networkId" : "solana/test"
}
]
},
{
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"contracts" : [
"networks" : [
{
"networkId" : "solana",
"address" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"networkId" : "solana/test",
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
}
]
@ -145,50 +137,47 @@
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"contracts" : [
"networks" : [
{
"networkId" : "solana",
"address" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"networkId" : "solana/test",
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
}
]
},
{
"id" : "binance/test",
"id" : "binancecoin",
"symbol" : "BNB",
"name" : "Binance",
"contracts" : [
"networks" : [
{
"networkId" : "binancecoin/test"
},
{
"networkId" : "binance-smart-chain/test"
}
]
},
{
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"contracts" : [
"networks" : [
{
"networkId" : "binancecoin",
"address" : "HEM-452",
"networkId" : "binancecoin/test",
"contractAddress" : "HEM-452",
"decimalCount" : 8
}
]
},
{
"id" : "bsc/test",
"symbol" : "BNB",
"name" : "Bsc",
"contracts" : [
]
},
{
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"contracts" : [
"networks" : [
{
"networkId" : "binance-smart-chain",
"address" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
}
]
@ -197,10 +186,10 @@
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"contracts" : [
"networks" : [
{
"networkId" : "binance-smart-chain",
"address" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
}
]
@ -209,10 +198,10 @@
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"contracts" : [
"networks" : [
{
"networkId" : "binance-smart-chain",
"address" : "0x64544969ed7ebf5f083679233325356ebe738930",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
}
]
@ -221,30 +210,32 @@
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"contracts" : [
"networks" : [
{
"networkId" : "binance-smart-chain",
"address" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
}
]
},
{
"id" : "ethereum/test",
"id" : "ethereum",
"symbol" : "ETH",
"name" : "Ethereum",
"contracts" : [
"networks" : [
{
"networkId" : "ethereum/test"
}
]
},
{
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"contracts" : [
"networks" : [
{
"networkId" : "ethereum",
"address" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"networkId" : "ethereum/test",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
}
]
@ -253,10 +244,10 @@
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"contracts" : [
"networks" : [
{
"networkId" : "ethereum",
"address" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"networkId" : "ethereum/test",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
}
]
@ -265,10 +256,10 @@
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"contracts" : [
"networks" : [
{
"networkId" : "ethereum",
"address" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"networkId" : "ethereum/test",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
}
]
@ -277,30 +268,32 @@
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"contracts" : [
"networks" : [
{
"networkId" : "ethereum",
"address" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"networkId" : "ethereum/test",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
}
]
},
{
"id" : "avalanche/test",
"id" : "avalanche-2",
"symbol" : "AVAX",
"name" : "Avalanche",
"contracts" : [
"networks" : [
{
"networkId" : "avalanche/test"
}
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"contracts" : [
"networks" : [
{
"networkId" : "avalanche",
"address" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"networkId" : "avalanche/test",
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
}
]
@ -309,30 +302,32 @@
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"contracts" : [
"networks" : [
{
"networkId" : "avalanche",
"address" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"networkId" : "avalanche/test",
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
}
]
},
{
"id" : "fantom/test",
"id" : "fantom",
"symbol" : "FTM",
"name" : "Fantom",
"contracts" : [
"networks" : [
{
"networkId" : "fantom/test"
}
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"contracts" : [
"networks" : [
{
"networkId" : "fantom",
"address" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"networkId" : "fantom/test",
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
}
]
@ -341,29 +336,36 @@
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"contracts" : [
"networks" : [
{
"networkId" : "fantom",
"address" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"networkId" : "fantom/test",
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
}
]
},
{
"id" : "bitcoin/test",
"id" : "bitcoin",
"symbol" : "BTC",
"name" : "Bitcoin",
"contracts" : [
"networks" : [
{
"networkId" : "bitcoin/test"
}
]
},
{
"id" : "stellar/test",
"id" : "stellar",
"symbol" : "XLM",
"name" : "Stellar",
"contracts" : [
"networks" : [
{
"networkId" : "stellar/test"
}
]
}
]
}
],
"total" : 0,
"imageHost" : "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,40 @@
package com.tangem.tap.common
import timber.log.Timber
class CompositionCounter(
val id: String,
count: Int = 0
) {
var count: Int = count
private set
fun increase(id: String): CompositionCounter {
if (this.id != id) return this
count += 1
return CompositionCounter(id, count)
}
}
class CompositionLogger(
private val recomposeViewId: String,
private val tag: String = recomposeViewId,
private var turnOnForIds: List<String> = listOf(recomposeViewId)
) {
val count: Int
get() = compositionCounter.count
private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId)
fun nextComposition() {
compositionCounter = compositionCounter.increase(recomposeViewId)
log("")
}
fun log(message: String) {
if (!turnOnForIds.contains(recomposeViewId)) return
Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message")
}
}

View file

@ -53,6 +53,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(

View file

@ -0,0 +1,75 @@
package com.tangem.tap.common
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.widget.LinearLayoutCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
/**
[REDACTED_AUTHOR]
*/
object TestActions {
// It used only for the test actions in debug or debug_beta builds
var testAmountInjectionForWalletManagerEnabled = false
/**
* @param isTestView - true must be used if you want to show or hide your view depends on BuildConfig
*/
fun initFor(view: View, actions: List<TestAction>, isTestView: Boolean = false) {
if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED)
if (!BuildConfig.TEST_ACTION_ENABLED) return
view.setOnClickListener {
store.dispatchDialogShow(AppDialog.TestActionsDialog(actions))
}
}
}
typealias TestAction = Pair<String, () -> Unit>
class TestActionsBottomSheetDialog(
private val appDialog: AppDialog.TestActionsDialog,
context: Context
) : BottomSheetDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(createContentView())
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = 400
setOnCancelListener {
store.dispatchDialogHide()
}
}
private fun createContentView(): View {
val actionButtons = mutableListOf<View>()
appDialog.actionsList.forEach { (actionName, action) ->
val view = Button(context).apply {
text = actionName
setOnClickListener {
action()
cancel()
}
}
actionButtons.add(view)
}
return LinearLayoutCompat(context).apply {
orientation = LinearLayoutCompat.VERTICAL
actionButtons.forEach { addView(it) }
}
}
}

View file

@ -2,9 +2,13 @@ package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
@ -66,6 +70,7 @@ fun PasteButton(
enabled: Boolean = true,
dpSize: DpSize = DpSize(40.dp, 40.dp),
onClick: () -> Unit,
tint: Color? = null,
content: @Composable (() -> Unit)? = null
) {
IconButton(
@ -75,14 +80,60 @@ fun PasteButton(
) {
when (content) {
null -> {
val icon = if (enabled) R.drawable.ic_paste else R.drawable.ic_paste_disabled
Icon(painterResource(id = icon), contentDescription = "Paste")
val tintColor = tint
?: colorResource(id = if (enabled) R.color.accent else R.color.accent_disabled)
Icon(
painterResource(id = R.drawable.ic_paste),
contentDescription = "Paste",
tint = tintColor,
)
}
else -> content()
}
}
}
@Composable
fun ClearButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
dpSize: DpSize = DpSize(40.dp, 40.dp),
onClick: () -> Unit,
tint: Color? = null,
content: @Composable (() -> Unit)? = null
) {
IconButton(
modifier = modifier.size(dpSize),
enabled = enabled,
onClick = onClick,
) {
when (content) {
null -> {
val tintColor = tint
?: colorResource(id = if (enabled) R.color.accent else R.color.accent_disabled)
Icon(
painterResource(id = R.drawable.ic_clear),
contentDescription = "Clear",
tint = tintColor,
)
}
else -> content()
}
}
}
/**
* Used for disable ripple if button is enable = false
*/
@Composable
fun ToggledRippleTheme(
isEnabled: Boolean,
content: @Composable () -> Unit,
) {
val theme = LocalRippleTheme provides if (isEnabled) LocalRippleTheme.current else NoRippleTheme()
CompositionLocalProvider(theme) { content() }
}
@Preview
@Composable
fun ButtonTest() {

View file

@ -1,24 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.tangem.domain.common.util.ValueDebouncer
/**
[REDACTED_AUTHOR]
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
*/
@Composable
fun <T> valueDebouncerAsState(
debounce: Long = 400,
onValueChanged: (T) -> Unit
): ValueDebouncer<T> {
return remember {
ValueDebouncer(
debounce = debounce,
onValueChanged = { changedValue ->
changedValue?.let { onValueChanged(it) }
},
)
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.tap.common.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.tangem.domain.common.util.ValueDebouncer
/**
[REDACTED_AUTHOR]
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
*/
@Composable
fun <T> valueDebouncerAsState(
initialValue: T,
debounce: Long = 600,
onEmitValueReceived: (T) -> Unit = {},
onValueChanged: (T) -> Unit,
): ValueDebouncer<T> {
return remember {
ValueDebouncer<T>(
initialValue = initialValue,
debounceDuration = debounce,
onEmitValueReceived = { emitValue ->
emitValue?.let { onEmitValueReceived(it) }
},
onValueChanged = { changedValue ->
changedValue?.let { onValueChanged(it) }
},
)
}
}
@Composable
fun <T> valueDebouncerNullableAsState(
initialValue: T?,
debounce: Long = 400,
onEmitValueReceived: (T?) -> Unit = {},
onValueChanged: (T?) -> Unit,
): ValueDebouncer<T?> {
return remember {
ValueDebouncer(
initialValue = initialValue,
debounceDuration = debounce,
onEmitValueReceived = onEmitValueReceived,
onValueChanged = onValueChanged,
)
}
}

View file

@ -17,18 +17,17 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.domain.DomainDialog
import com.tangem.domain.DomainStateDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@Composable
fun ComposeDialogManager() {
val dialogSate = remember { mutableStateOf<DomainStateDialog?>(null) }
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
val subscriber = remember {
object : StoreSubscriber<DomainGlobalState> {
override fun newState(state: DomainGlobalState) {
@ -52,17 +51,17 @@ fun ComposeDialogManager() {
}
@Composable
private fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
if (dialogState.value == null) return
val context = LocalContext.current
val errorConverter = remember { DomainErrorConverter(context) }
val errorConverter = remember { ModuleMessageConverter(context) }
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
when (val dialog = dialogState.value) {
is DomainDialog.DialogError -> ErrorDialog(
title = stringResource(id = R.string.common_error),
body = errorConverter.convertError(dialog.error),
body = errorConverter.convert(dialog.error),
onDismissRequest
)
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)

View file

@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
@ -30,13 +28,13 @@ fun <T> OutlinedSpinner(
onClose: VoidCallback = {}
) {
val rIsExpanded = remember { mutableStateOf(false) }
val rSelectedItem = remember { mutableStateOf(selectedItem.value) }
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
if (!selectedItem.isUserInput) {
rSelectedItem.value = selectedItem.value
stateSelectedItem.value = selectedItem.value
}
val onItemSelectedInternal: (T) -> Unit = {
rSelectedItem.value = it
val onDropDownItemSelectedInternal: (T) -> Unit = {
stateSelectedItem.value = it
rIsExpanded.value = false
onItemSelected(it)
}
@ -49,23 +47,23 @@ fun <T> OutlinedSpinner(
expanded = rIsExpanded.value,
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
) {
ProvideTextStyle(value = TextStyle(color = Color.Blue)) {
OutlinedTextField(
modifier = modifier,
readOnly = true,
enabled = isEnabled,
value = textFieldConverter(rSelectedItem.value),
onValueChange = {},
label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
)
}
OutlinedTextField(
modifier = modifier,
readOnly = true,
enabled = isEnabled,
value = textFieldConverter(stateSelectedItem.value),
onValueChange = {},
label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
)
if (!isEnabled) return@ExposedDropdownMenuBox
ExposedDropdownMenu(
expanded = rIsExpanded.value,
onDismissRequest = onDismissRequest,
) {
itemList.forEach { item ->
DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) {
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
when (dropdownItemView) {
null -> Text(textFieldConverter(item))
else -> dropdownItemView(item)
@ -83,7 +81,7 @@ fun TestSpinnerPreview() {
OutlinedSpinner(
label = "Blockchain name",
itemList = listOf(Blockchain.values()),
selectedItem = Field.Data(Blockchain.Avalanche),
selectedItem = Field.Data(Blockchain.Avalanche, false),
onItemSelected = {},
)
}

View file

@ -15,15 +15,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.domain.DomainError
import com.tangem.domain.ErrorConverter
import com.tangem.common.module.ModuleError
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.CompositionLogger
import com.tangem.tap.common.compose.extensions.stringResourceDefault
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
/**
[REDACTED_AUTHOR]
@ -31,7 +34,7 @@ import com.tangem.tap.common.compose.extensions.stringResourceDefault
@Composable
fun OutlinedTextFieldWidget(
modifier: Modifier = Modifier,
textFieldData: Field.Data<String>,
fieldData: Field.Data<String>,
labelId: Int? = null,
label: String = "",
placeholderId: Int? = null,
@ -40,8 +43,8 @@ fun OutlinedTextFieldWidget(
isEnabled: Boolean = true,
isVisible: Boolean = true,
isLoading: Boolean = false,
error: DomainError? = null,
errorConverter: ErrorConverter<String>? = null,
error: ModuleError? = null,
errorConverter: ModuleMessageConverter? = null,
debounceTextChanges: Long = 400,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
@ -54,7 +57,7 @@ fun OutlinedTextFieldWidget(
) {
OutlinedProgressTextField(
modifier = modifier,
textFieldData = textFieldData,
fieldData = fieldData,
label = stringResourceDefault(labelId, label),
placeholder = stringResourceDefault(placeholderId, placeholder),
trailingIcon = trailingIcon,
@ -73,39 +76,91 @@ fun OutlinedTextFieldWidget(
@Composable
private fun OutlinedProgressTextField(
modifier: Modifier = Modifier,
textFieldData: Field.Data<String>,
fieldData: Field.Data<String>,
label: String = "",
placeholder: String = "",
isEnabled: Boolean = true,
isLoading: Boolean = false,
error: DomainError? = null,
error: ModuleError? = null,
debounce: Long = 400,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
trailingIcon: @Composable (() -> Unit)? = null,
onTextChanged: (String) -> Unit,
) {
val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged)
val rText = remember { mutableStateOf(textFieldData.value) }
val logger = remember {
CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена"))
}
logger.nextComposition()
fun updateFieldValueAndEmmit(value: String) {
rText.value = value
rTextDebouncer.emmit(value)
}
// This action came from redux. Update the field value and send a new event as if from the user
if (!textFieldData.isUserInput) {
updateFieldValueAndEmmit(textFieldData.value)
val textValueState = remember { mutableStateOf(fieldData.value) }
val textDebouncer = valueDebouncerAsState(
initialValue = fieldData.value,
debounce = debounce,
onEmitValueReceived = {
logger.log("DEBOUNCER: onEmitValueReceived: [$it]")
logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]")
textValueState.value = it
},
onValueChanged = {
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
onTextChanged(it)
})
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]")
logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]")
logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]")
logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]")
if (!fieldData.isUserInput) {
// initial value is not from an user
val isNotUserInput = "-- IS NOT USER INPUT"
logger.log("recompose $isNotUserInput")
if (textValueState.value == fieldData.value) {
logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле")
} else {
logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле")
if ((textDebouncer.emittedValue != textDebouncer.debounced) || textDebouncer.emitsCountBeforeDebounce > 0) {
logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE")
} else {
logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные")
if (textValueState.value != textDebouncer.emittedValue || textValueState.value != textDebouncer.debounced) {
logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer")
if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) {
logger.log("$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
textValueState.value = fieldData.value
} else {
logger.log("$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
textValueState.value = fieldData.value
}
} else {
logger.log("$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
textValueState.value = fieldData.value
}
}
}
}
logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]")
Box {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(),
value = rText.value,
onValueChange = ::updateFieldValueAndEmmit,
value = textValueState.value,
onValueChange = {
logger.log("WIDGET: textDebouncer.emmit([$it])")
textDebouncer.emmit(it)
},
keyboardOptions = keyboardOptions,
label = { Text(label) },
placeholder = { Text(placeholder) },
placeholder = {
Text(
text = placeholder,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
trailingIcon = trailingIcon,
singleLine = true,
enabled = isEnabled,
@ -120,13 +175,12 @@ private fun OutlinedProgressTextField(
visible = isLoading,
) { LinearProgressIndicator() }
}
}
@Composable
private fun AnimatedErrorView(
error: DomainError? = null,
errorConverter: ErrorConverter<String>,
error: ModuleError? = null,
errorConverter: ModuleMessageConverter,
) {
AnimatedVisibility(
visible = error != null,
@ -134,7 +188,7 @@ private fun AnimatedErrorView(
exit = slideOutVertically() + fadeOut(),
) {
error?.let {
ErrorView(errorConverter.convertError(it), style = TextStyle(fontSize = 14.sp))
ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp))
}
}
}
@ -142,20 +196,14 @@ private fun AnimatedErrorView(
@Preview
@Composable
fun OutlinedTextFieldWithErrorTest() {
val converter = remember {
object : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
return "Hello, i'am the error: ${error::class.java.simpleName}"
}
}
}
val context = LocalContext.current
val converter = remember { ModuleMessageConverter(context) }
class SimpleError(
override val code: Int = 1,
override val message: String = "Error message",
override val data: Any? = null,
) : DomainError
) : ModuleError
val modifier = Modifier
.fillMaxWidth()
@ -166,7 +214,7 @@ fun OutlinedTextFieldWithErrorTest() {
) {
OutlinedTextFieldWidget(
modifier = modifier,
textFieldData = Field.Data(""),
fieldData = Field.Data("", false),
label = "First label",
placeholder = "1 placeholder",
error = null,
@ -174,7 +222,7 @@ fun OutlinedTextFieldWithErrorTest() {
) {}
OutlinedTextFieldWidget(
modifier = modifier,
textFieldData = Field.Data("First"),
fieldData = Field.Data("First", false),
label = "First label",
placeholder = "1 placeholder",
error = null,
@ -182,7 +230,7 @@ fun OutlinedTextFieldWithErrorTest() {
) {}
OutlinedTextFieldWidget(
modifier = modifier,
textFieldData = Field.Data("First"),
fieldData = Field.Data("First", false),
label = "First label",
placeholder = "1 placeholder",
isLoading = true,
@ -191,7 +239,7 @@ fun OutlinedTextFieldWithErrorTest() {
) {}
OutlinedTextFieldWidget(
modifier = modifier,
textFieldData = Field.Data("First"),
fieldData = Field.Data("First", false),
label = "First label",
placeholder = "1 placeholder",
error = SimpleError(),

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.compose
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
[REDACTED_AUTHOR]
*/
class NoRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = Color.Unspecified
@Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
}

View file

@ -0,0 +1,53 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.common.module.ModuleMessage
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun AddCustomTokenWarning(
modifier: Modifier = Modifier,
warning: ModuleMessage,
converter: ModuleMessageConverter
) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.warning_warning),
elevation = 4.dp,
) {
Column(
modifier = Modifier.padding(16.dp),
) {
Text(
text = stringResource(id = R.string.common_warning),
color = colorResource(id = R.color.white),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
SpacerH8()
Text(
text = converter.convert(warning),
color = colorResource(id = R.color.white),
fontSize = 13.sp,
lineHeight = 18.sp
)
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.extensions
import androidx.annotation.DrawableRes
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.remove
import com.tangem.wallet.R
@DrawableRes
@ -22,6 +23,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_round
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_round
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round
else -> R.drawable.ic_tangem_logo
}
}
@ -44,6 +46,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_no_color
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color
else -> R.drawable.ic_tangem_logo
}
}
@ -55,4 +58,7 @@ fun Blockchain.getNetworkName(): String {
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
else -> ""
}
}
}
val Blockchain.fullNameWithoutTestnet
get() = this.fullName.remove(" Testnet")

View file

@ -26,6 +26,8 @@ import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.R
private class Navigation
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,

View file

@ -1,8 +1,8 @@
package com.tangem.tap.common.extensions
import com.tangem.common.extensions.isZero
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
@ -10,7 +10,7 @@ import java.text.DecimalFormatSymbols
import java.util.*
fun BigDecimal.toFormattedString(
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat()
@ -26,7 +26,7 @@ fun BigDecimal.toFormattedCurrencyString(
decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN,
limitNumberOfDecimals: Boolean = true
): String {
val decimalsForRounding = if (limitNumberOfDecimals){
val decimalsForRounding = if (limitNumberOfDecimals) {
if (decimals > 8) 8 else decimals
} else {
decimals
@ -52,7 +52,7 @@ fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: FiatCurrencyName): String
return "≈ ${fiatCurrencyName} $this"
}
fun FiatCurrency.toFormattedString(): String = "${this.name} (${this.symbol}) - ${this.sign}"
fun CurrenciesResponse.Currency.toFormattedString(): String = "${this.name} (${this.code}) - ${this.unit}"
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.services.Result
import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
@ -20,8 +21,9 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
if (isDemoWallet()) {
if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)
} else {
update()
@ -37,10 +39,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken())
if (exception is BlockchainSdkError.AccountNotFound && amountToCreateAccount != null) {
Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString()))
Result.Failure(TapError.WalletManager.NoAccountError(amountToCreateAccount.toString()))
} else {
val message = exception.localizedMessage ?: "An error has occurred. Try later"
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
Result.Failure(TapError.WalletManager.InternalError(message))
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.moduleMessage
import android.content.Context
import com.tangem.common.module.ModuleMessage
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainModuleMessage
import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter
class ModuleMessageConverter(
private val context: Context
) : ModuleMessageConverter<ModuleMessage, String> {
override fun convert(message: ModuleMessage): String {
val convertedMessage = when (message) {
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
else -> null
}
return convertedMessage ?: convertUnknownMessage(message)
}
private fun convertUnknownMessage(message: ModuleMessage): String {
return "Unknown message: ${message::class.java.simpleName}"
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.tap.common.moduleMessage.domain
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainError
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class DomainErrorConverter(
private val context: Context
) : ModuleMessageConverter<DomainError, String?> {
override fun convert(message: DomainError): String? = when (message) {
is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message)
else -> null
}
}
private class AddCustomTokenConverter(
private val context: Context
) : ModuleMessageConverter<DomainError, String?> {
override fun convert(message: DomainError): String? {
val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException()
val rawMessage = when (customTokenError) {
AddCustomTokenError.Warning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenError.Warning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
AddCustomTokenError.InvalidDecimalsCount -> {
context.getString(R.string.custom_token_creation_error_wrong_decimals, 30)
}
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field
else -> null
}
return when (rawMessage) {
is Int -> context.getString(rawMessage)
is String -> rawMessage
else -> null
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common.moduleMessage.domain
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainError
import com.tangem.domain.DomainModuleMessage
/**
[REDACTED_AUTHOR]
*/
class DomainMessageConverter(
private val context: Context
) : ModuleMessageConverter<DomainModuleMessage, String?> {
override fun convert(message: DomainModuleMessage): String? {
return when (message) {
is DomainError -> DomainErrorConverter(context).convert(message)
else -> null
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.NetworkServices
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.NavigationState
@ -54,6 +55,9 @@ data class AppState(
val domainState: DomainState
get() = domainStore.state
val domainNetworks: NetworkServices
get() = domainState.globalState.networkServices
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(

View file

@ -4,12 +4,14 @@ import android.content.Context
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.domain.ArgError
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.notificationsHandler
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
@ -29,7 +31,17 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
fun showNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar -> snackbar.show() }
.also { snackbar -> snackbar.show() }
}
}
fun showDebugNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar ->
snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning))
snackbar.show()
}
}
}
@ -51,6 +63,12 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
val message = builder(errorList.map { getMessageString(context, it.first, it.second) })
showNotification(message)
}
fun showDebugErrorNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
showDebugNotification(getMessageString(it.context, message, args))
}
}
}
fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
@ -74,17 +92,37 @@ private fun handleNotificationAction(action: Action) {
if (action is Debug && !BuildConfig.DEBUG) return
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is NotificationAction -> {
notificationsHandler?.showNotification(action.messageResource)
}
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
when (action) {
is Debug -> {
val args = (action.error as? ArgError)?.args ?: listOf()
when (action) {
is DebugNotification -> {
notificationsHandler?.showNotification(action.error.messageResource, args)
}
is DebugToastNotification -> {
notificationsHandler?.showToastNotification(action.error.messageResource, args)
}
is DebugErrorAction -> {
notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args)
}
}
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.messageResource, args)
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.messageResource, args)
}
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.TestAction
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
@ -17,4 +18,8 @@ sealed class AppDialog : StateDialog {
val currency: Currency,
val addressData: AddressData,
) : AppDialog()
data class TestActionsDialog(
val actionsList: List<TestAction>
) : AppDialog()
}

View file

@ -4,8 +4,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -102,12 +100,10 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
domainStore.dispatch(DomainGlobalAction.SetScanResponse(result.data))
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> {
domainStore.dispatch(DomainGlobalAction.SetScanResponse(null))
action.onFailure?.invoke(result.error)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
@ -33,8 +35,10 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.ScanFailsCounter.Reset -> {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse ->
is GlobalAction.SaveScanNoteResponse ->{
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
globalState.copy(scanResponse = action.scanResponse)
}
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency)
}

View file

@ -10,7 +10,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
@ -20,7 +19,6 @@ data class GlobalState(
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,

View file

@ -0,0 +1,83 @@
package com.tangem.tap.domain
import com.tangem.common.services.Result
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.store
import java.math.BigDecimal
//TODO: refactoring: move to domain
class RatesRepository {
private val tangemTechService: TangemTechService
get() = store.state.domainNetworks.tangemTechService
private val throttler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
suspend fun loadFiatRate(currencyId: String, coinsList: List<Currency>): Result<RatesResult> {
// get and submit previous result of equivalents.
val throttledResult = coinsList.filter { throttler.isStillThrottled(it) }.map {
Pair(it, throttler.geValue(it))
}
if (throttledResult.isNotEmpty()) {
return handleFiatRatesResult(throttledResult.toMap())
}
val currenciesToUpdate = coinsList.filter { !throttler.isStillThrottled(it) }
val coinIds = currenciesToUpdate.mapNotNull { it.coinId }.distinct()
if (coinIds.isEmpty()) return EMPTY_RESULT
return when (val result = tangemTechService.rates(currencyId, coinIds)) {
is Result.Success -> {
val ratesResultList: Map<String, Result<BigDecimal>> = result.data.rates.mapValues {
Result.Success(it.value.toBigDecimal())
}
val updatedCurrencies = mutableMapOf<Currency, Result<BigDecimal>?>()
currenciesToUpdate.forEach { currency ->
ratesResultList[currency.coinId]?.let {
updatedCurrencies[currency] = it
throttler.updateThrottlingTo(currency)
throttler.setValue(currency, it)
}
}
handleFiatRatesResult(updatedCurrencies)
}
is Result.Failure -> Result.Failure(result.error)
}
}
private fun handleFiatRatesResult(rates: Map<Currency, Result<BigDecimal>?>): Result.Success<RatesResult> {
val success = mutableMapOf<Currency, BigDecimal>()
val failures = mutableMapOf<Currency, Throwable>()
rates.mapNotNull { (currency, priceResult) ->
when (priceResult) {
is Result.Success -> success[currency] = priceResult.data
is Result.Failure -> failures[currency] = priceResult.error
else -> null
}
}
return Result.Success(success to failures)
}
fun clear() {
throttler.clear()
}
companion object {
val EMPTY_RESULT = Result.Success(Pair(
mutableMapOf<Currency, BigDecimal>(),
mutableMapOf<Currency, Throwable>()
))
}
}
typealias RatesResult = Pair<MutableMap<Currency, BigDecimal>, MutableMap<Currency, Throwable>>
val RatesResult.loadedRates
get() = this.first
val RatesResult.failedRates
get() = this.second

View file

@ -43,7 +43,8 @@ sealed class TapError(
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
}
sealed class WalletManagerUpdate {
sealed class WalletManager {
object CreationError: CustomError("Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
class InternalError(message: String): CustomError(message)
object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable)

View file

@ -1,50 +1,40 @@
package com.tangem.tap.domain
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.common.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.ThrottlerWithValues
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
private val coinMarketCapService = CoinMarketCapService()
val rates: RatesRepository = RatesRepository()
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler = ThrottlerWithValues<Blockchain, Result<Wallet>>(10000)
private val fiatRatesThrottler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
suspend fun loadWalletData(walletManager: WalletManager) {
val blockchain = walletManager.wallet.blockchain
@ -52,73 +42,37 @@ class TapWalletManager {
val result = if (walletManagersThrottler.isStillThrottled(blockchain)) {
walletManagersThrottler.geValue(blockchain)!!
} else {
updateThrottlingForWalletManager(walletManager)
val safeUpdateResult = walletManager.safeUpdate()
walletManagersThrottler.updateThrottlingTo(blockchain)
walletManagersThrottler.setValue(blockchain, safeUpdateResult)
safeUpdateResult
}
when (result) {
is Result.Success -> {
checkForRentWarning(walletManager)
dispatchOnMain(WalletAction.LoadWallet.Success(result.data, blockchainNetwork))
}
is Result.Failure -> {
when (result.error) {
is TapError.WalletManagerUpdate.NoAccountError -> {
dispatchOnMain(
WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
)
)
is TapError.WalletManager.NoAccountError -> {
dispatchOnMain(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManager.NoAccountError).customMessage
))
}
else -> {
dispatchOnMain(
WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
)
)
dispatchOnMain(WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
))
}
}
}
}
}
private suspend fun updateThrottlingForWalletManager(walletManager: WalletManager): Result<Wallet> {
val newResult = walletManager.safeUpdate()
val blockchain = walletManager.wallet.blockchain
walletManagersThrottler.updateThrottlingTo(blockchain)
walletManagersThrottler.setValue(blockchain, newResult)
return newResult
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
val currencies = wallet.getTokens()
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
loadFiatRate(fiatCurrency, currencies)
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencies: List<Currency>) {
// get and submit previous result of equivalents.
val throttledResult = currencies.filter { fiatRatesThrottler.isStillThrottled(it) }.map {
Pair(it, fiatRatesThrottler.geValue(it))
}
if (throttledResult.isNotEmpty()) handleFiatRatesResult(throttledResult)
val toUpdate = currencies.filter { !fiatRatesThrottler.isStillThrottled(it) }
toUpdate.forEach {
val result = coinMarketCapService.getRate(it.currencySymbol, fiatCurrency)
if (result is Result.Success) {
fiatRatesThrottler.updateThrottlingTo(it)
fiatRatesThrottler.setValue(it, result)
}
handleFiatRatesResult(listOf(it to result))
}
}
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
// fiatRatesThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
updateConfigManager(data)
@ -169,22 +123,18 @@ class TapWalletManager {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, primaryWalletManager)
} else {
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
listOf(primaryWalletManager)
)
)
dispatchOnMain(WalletAction.MultiWallet.AddBlockchains(
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
listOf(primaryWalletManager)
))
}
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, null)
}
}
dispatchOnMain(
WalletAction.LoadWallet(),
WalletAction.LoadFiatRate()
)
dispatchOnMain(WalletAction.LoadWallet())
dispatchOnMain(WalletAction.LoadFiatRate())
}
private suspend fun loadMultiWalletData(
@ -193,7 +143,9 @@ class TapWalletManager {
primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
val savedCurrencies = currenciesRepository.loadSavedCurrencies(scanResponse.card.cardId, scanResponse.card.derivationStyle)
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
)
if (savedCurrencies.isEmpty()) {
if (primaryBlockchain != null && primaryWalletManager != null) {
@ -208,7 +160,6 @@ class TapWalletManager {
)
} else {
val derivationStyle = scanResponse.card.derivationStyle
val blockchainNetworks = listOf(
BlockchainNetwork(Blockchain.Bitcoin, scanResponse.card),
BlockchainNetwork(Blockchain.Ethereum, scanResponse.card)
@ -224,15 +175,15 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(blockchainNetworks, walletManagers),
)
}
dispatchOnMain(
WalletAction.MultiWallet.FindBlockchainsInUse,
WalletAction.MultiWallet.FindTokensInUse,
)
// dispatchOnMain(
// WalletAction.MultiWallet.FindBlockchainsInUse,
// WalletAction.MultiWallet.FindTokensInUse,
// )
} else {
val walletManagers = if (
primaryTokens.isNotEmpty() &&
primaryWalletManager != null &&
primaryBlockchain != null
primaryBlockchain != null && primaryBlockchain != Blockchain.Unknown
) {
val blockchainsWithoutPrimary = savedCurrencies.filterNot { it.blockchain == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(
@ -246,7 +197,9 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
if (it.tokens.isNotEmpty()) {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
}
}
}
}
@ -285,53 +238,6 @@ class TapWalletManager {
else -> null
}
}
private suspend fun checkForRentWarning(walletManager: WalletManager) {
val rentProvider = walletManager as? RentProvider ?: return
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean =
balance < rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs =
walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
if (!show) return
val currency = walletManager.wallet.blockchain.currency
dispatchOnMain(
WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
)
)
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<Currency, Result<BigDecimal>?>>) {
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
dispatchOnMain(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
null -> {}
}
}
}
}
fun Wallet.getFirstToken(): Token? {

View file

@ -21,7 +21,7 @@ data class WarningMessage(
val messageFormatArg: String? = null
) {
val blockchainList: List<Blockchain>? by lazy {
blockchains?.map { Blockchain.fromId(it.toUpperCase()) }
blockchains?.map { Blockchain.fromId(it.uppercase()) }
}

View file

@ -4,7 +4,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import java.util.*
/**
[REDACTED_AUTHOR]
@ -16,6 +18,11 @@ class WarningMessagesManager(
private val warningsList: MutableList<WarningMessage> = mutableListOf()
fun load(onComplete: VoidCallback? = null) {
// exclude annoying remote debug warnings
if (BuildConfig.DEBUG) {
onComplete?.invoke()
return
}
warningLoader.load { remoteList ->
warningsList.clear()
warningsList.addAll(remoteList)
@ -181,6 +188,31 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun restoreFundsWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Warning,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.alert_title,
messageResId = R.string.alert_funds_restoration_message,
origin = WarningMessage.Origin.Local,
buttonTextId = R.string.warning_button_learn_more
)
const val REMAINING_SIGNATURES_WARNING = 10
private const val RESTORE_FUNDS_GUIDE_URL_RU =
"https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet"
private const val RESTORE_FUNDS_GUIDE_URL_EN =
"https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet"
fun getRestoreFundsGuideUrl(locale: String): String {
return if (locale == Locale("ru").language) {
RESTORE_FUNDS_GUIDE_URL_RU
} else {
RESTORE_FUNDS_GUIDE_URL_EN
}
}
}
}

View file

@ -21,12 +21,15 @@ fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (!isBuyAllowed) return false
//TODO: temporary, for the 3.32 release, unlock all buy button
return true
return when (currency) {
is Currency.Blockchain -> {
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
blockchain == Blockchain.Unknown || blockchain == Blockchain.BSC -> false
blockchain == Blockchain.Unknown -> false
else -> availableToBuy.contains(currency.currencySymbol)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.tap.domain.statePrinter
import com.tangem.common.extensions.toHexString
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.redux.state.StringStateConverter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class ScanResponseConverter : StringStateConverter<AppState> {
private val converter = MoshiJsonConverter.INSTANCE
override fun convert(stateHolder: AppState): String {
val scanResponse = stateHolder.globalState.scanResponse ?: return "NULL"
val scanResponseMap = mutableMapOf<String, Any?>()
val derivedKeysMap = mutableMapOf<String, Any?>()
scanResponse.derivedKeys.forEach { (keyWalletPubKey, mapExPubKeys) ->
val exPubKeysMap = mapExPubKeys.entries.map { (derivationPath, exPubKey) ->
mapOf(
"derivationPath" to derivationPath.rawPath,
"extendedPublicKey" to mapOf(
"publicKey" to exPubKey.publicKey.toHexString(),
"chainCode" to exPubKey.chainCode.toHexString(),
),
)
}
derivedKeysMap[keyWalletPubKey.bytes.toHexString()] = exPubKeysMap
}
scanResponseMap["derivedKeys"] = derivedKeysMap
val json = converter.prettyPrint(scanResponseMap)
return json
}
}
fun printScanResponseState() {
val stringState = ScanResponseConverter().convert(store.state)
Timber.d(stringState)
}

View file

@ -0,0 +1,69 @@
package com.tangem.tap.domain.statePrinter
import com.tangem.blockchain.common.Wallet
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.redux.state.StringStateConverter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class WalletStateConverter : StringStateConverter<AppState> {
private val converter = MoshiJsonConverter.INSTANCE
override fun convert(stateHolder: AppState): String {
val walletState = stateHolder.walletState
val walletManagers = mutableListOf<Map<String, Any?>>()
walletState.walletManagers.forEach {
val wallet = it.wallet
val walletManagerMap = mutableMapOf<String, Any?>()
walletManagerMap["walletManager"] = mapOf<String, Any>(
"wallet" to convertWallet(it.wallet),
)
walletManagers.add(walletManagerMap)
}
val json = converter.prettyPrint(walletManagers)
return json
}
private fun convertWallet(wallet: Wallet): MutableMap<String, Any> {
val walletMap = mutableMapOf<String, Any>()
val amounts = mutableMapOf<String, Any>()
wallet.amounts.forEach { (type, amount) ->
val amountMap = mapOf<String, Any?>(
"value" to amount.value?.toPlainString(),
"currencySymbol" to amount.currencySymbol,
"decimals" to amount.decimals,
"type" to amount.type::class.java.simpleName,
)
amounts[type::class.java.simpleName] = amountMap
}
val publicKeyMap = mapOf(
"seedKey" to wallet.publicKey.seedKey,
"derivedKey" to wallet.publicKey.derivedKey,
"derivationPath" to wallet.publicKey.derivationPath?.rawPath,
"blockchainKey" to wallet.publicKey.blockchainKey,
)
walletMap["address"] = wallet.address
walletMap["blockchain"] = wallet.blockchain.name
walletMap["curve"] = wallet.blockchain.getSupportedCurves()[0].curve
walletMap["publicKey"] = publicKeyMap
walletMap["amounts"] = amounts
walletMap["addresses"] = wallet.addresses.toString()
walletMap["cardId"] = wallet.cardId
return walletMap
}
}
fun printWalletState() {
val stringState = WalletStateConverter().convert(store.state)
Timber.d(stringState)
}

View file

@ -15,7 +15,6 @@ import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.*
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
@ -228,7 +227,7 @@ private class ScanWalletProcessor(
private fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.derivationStyle).toMutableList()
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
mutableListOf(

View file

@ -100,19 +100,16 @@ class CurrenciesRepository(val context: Application) {
fun loadSavedCurrencies(
cardId: String,
derivationStyle: DerivationStyle? = null
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies(cardId)
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: loadSavedCurrenciesOldWay(
cardId,
derivationStyle
)
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
}
}
@ -126,12 +123,27 @@ class CurrenciesRepository(val context: Application) {
}
}
private fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
return try {
loadSavedCurrenciesOldWay(
cardId,
isHdWalletSupported
)
} catch (exception: Exception) {
emptyList()
}
}
private fun loadSavedCurrenciesOldWay(
cardId: String, derivationStyle: DerivationStyle?
cardId: String, isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val currencies = getSupportedTokens()
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
@ -141,7 +153,7 @@ class CurrenciesRepository(val context: Application) {
.map {
val token = it.toToken()
val id = currencies
.find { it.contracts?.find { it.address == token.contractAddress } != null }
.find { it.contracts.find { it.address == token.contractAddress } != null }
?.id
token.copy(id = id)
}
@ -168,7 +180,8 @@ class CurrenciesRepository(val context: Application) {
fun getSupportedTokens(isTestNet: Boolean = false): List<Currency> {
val fileName = if (isTestNet) "testnet_tokens" else "tokens"
val json = context.assets.readJsonFileToString(fileName)
return currenciesAdapter.fromJson(json)!!.tokens.map { Currency.fromJsonObject(it) }
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun loadTokensJson(blockchain: Blockchain): String? {

View file

@ -9,20 +9,20 @@ data class CurrencyFromJson(
val id: String,
val name: String,
val symbol: String,
val contracts: List<ContractFromJson>? = null
val networks: List<ContractFromJson>? = null
)
@JsonClass(generateAdapter = true)
data class ContractFromJson(
val networkId: String,
val address: String,
val decimalCount: Int
val contractAddress: String?,
val decimalCount: Int?
)
@JsonClass(generateAdapter = true)
data class CurrenciesFromJson(
val imageHost: String,
val tokens: List<CurrencyFromJson>
val imageHost: String?,
val coins: List<CurrencyFromJson>
)
@ -35,7 +35,7 @@ data class Currency(
val name: String,
val symbol: String,
val iconUrl: String,
val contracts: List<Contract>?
val contracts: List<Contract>
) {
companion object {
@ -45,7 +45,7 @@ data class Currency(
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id),
contracts = currency.contracts?.toContracts()
contracts = currency.networks?.toContracts() ?: emptyList()
)
}
}
@ -54,8 +54,8 @@ data class Currency(
data class Contract(
val networkId: String,
val blockchain: Blockchain,
val address: String,
val decimalCount: Int,
val address: String?,
val decimalCount: Int?,
val iconUrl: String,
) {
@ -65,7 +65,7 @@ data class Contract(
return Contract(
networkId = contract.networkId,
blockchain = blockchain,
address = contract.address,
address = contract.contractAddress,
decimalCount = contract.decimalCount,
iconUrl = getIconUrl(contract.networkId)
)

View file

@ -85,7 +85,7 @@ class DemoConfig {
)
val demoCardIds: List<String> by lazy {
val demoIds = (releaseDemoCardIds + testDemoCardIds).toMutableList()
val demoIds = getReleaseIds().toMutableList()
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
return@lazy demoIds.distinct()
@ -103,6 +103,12 @@ class DemoConfig {
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
?: Amount(BigDecimal.ZERO, blockchain).copy()
private fun getReleaseIds(): List<String> {
return (releaseDemoCardIds +
releaseDemoCardIds_19042022 +
testDemoCardIds).distinct()
}
private val releaseDemoCardIds = mutableListOf<String>(
// Tangem Wallet:
"AC01000000041100",
@ -145,14 +151,113 @@ class DemoConfig {
"AB02000000051059",
)
// https://tangem.slack.com/archives/GMXC6PP71/p1650360610759269
private val releaseDemoCardIds_19042022 = listOf(
// Wallet
"AC01000000045754",
"AC01000000041662",
"AC01000000041647",
"AC01000000041209",
"AC01000000042462",
"AC01000000041100",
"AC01000000041621",
"AC01000000045960",
"AC01000000041092",
"AC01000000041217",
"AC01000000013489",
"AC01000000028610",
"AC01000000028701",
"AC01000000028578",
"AC01000000027281",
"AC01000000027216",
"AC01000000028594",
"AC01000000028602",
"AC01000000028636",
"AC01000000013968",
"AC01000000027208",
"AC01000000013471",
"AC01000000028586",
"AC01000000013703",
"AC01000000028628",
"AC01000000028693",
"AC01000000028685",
"AC01000000013950",
"AC01000000013828",
"AC01000000013497",
"AC01000000013836",
"AC01000000013505",
// Note BTC
"AB01000000059608",
"AB01000000046647",
"AB01000000046571",
"AB01000000046746",
"AB01000000059574",
"AB01000000046753",
"AB01000000046605",
"AB01000000046761",
"AB01000000046720",
"AB01000000046530",
"AB01000000016475",
"AB01000000016483",
"AB01000000016491",
"AB01000000020709",
"AB01000000020717",
"AB01000000015550",
"AB01000000015394",
"AB01000000016079",
"AB01000000016087",
"AB01000000016095",
"AB01000000020915",
"AB01000000017184",
"AB01000000020907",
"AB01000000017192",
"AB01000000016210",
"AB01000000016111",
"AB01000000016103",
"AB01000000015766",
"AB01000000015774",
"AB01000000015782",
"AB01000000022598",
"AB01000000022580",
// Note ETH
"AB02000000051083",
"AB02000000051059",
"AB02000000051158",
"AB02000000050986",
"AB02000000051026",
"AB02000000050960",
"AB02000000051042",
"AB02000000051091",
"AB02000000051034",
"AB02000000051133",
"AB02000000019924",
"AB02000000019932",
"AB02000000022092",
"AB02000000022282",
"AB02000000023983",
"AB02000000023439",
"AB02000000020328",
"AB02000000020310",
"AB02000000021565",
"AB02000000022357",
"AB02000000023355",
"AB02000000022324",
"AB02000000022100",
"AB02000000019999",
"AB02000000020013",
"AB02000000020005",
"AB02000000020021",
"AB02000000020039",
"AB02000000020278",
"AB02000000020252",
"AB02000000018652",
"AB02000000018561",
)
private val testDemoCardIds = listOf(
"FB20000000000186", // Note ETH
"FB10000000000196", // Note BTC
"FB30000000000176", // Wallet
//TODO: delete bellow ids before 3.28 release
"AB01000000045060", // Note BTC
"AB02000000045028", // Note ETH
"AC79000000000004", // Wallet 4.46
)
private val debugTestDemoCardIds = listOf<String>(

View file

@ -4,11 +4,12 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
import org.rekotlin.Action
@ -20,6 +21,7 @@ sealed class DetailsAction : Action {
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
val tangemTechService: TangemTechService,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -46,7 +48,7 @@ sealed class DetailsAction : Action {
object CreateBackup : DetailsAction()
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
data class SetCurrencies(val currencies: List<CurrenciesResponse.Currency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()

View file

@ -19,7 +19,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletM
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -33,7 +32,7 @@ class DetailsMiddleware {
{ next ->
{ action ->
when (action) {
is DetailsAction.PrepareScreen -> prepareData()
is DetailsAction.PrepareScreen -> prepareData(action)
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
@ -68,23 +67,24 @@ class DetailsMiddleware {
}
}
private fun prepareData() {
private fun prepareData(action: DetailsAction.PrepareScreen) {
val fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(storedFiatCurrencies))
}
scope.launch {
val loadedCurrencies = preferencesStorage.getFiatCurrencies()
if (loadedCurrencies.isNullOrEmpty()) {
val response = CoinMarketCapService().getFiatCurrencies()
withContext(Dispatchers.Main) {
when (response) {
is Result.Success -> {
preferencesStorage.saveFiatCurrencies(response.data)
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(response.data))
}
val tangemTechService = action.tangemTechService
when (val result = tangemTechService.currencies()) {
is Result.Success -> {
val currenciesList = result.data.currencies
if (currenciesList.isNotEmpty() && currenciesList.toSet() != storedFiatCurrencies.toSet()) {
fiatCurrenciesPrefStorage.save(currenciesList)
dispatchOnMain(DetailsAction.AppCurrencyAction.SetCurrencies(currenciesList))
}
}
} else {
withContext(Dispatchers.Main) {
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(loadedCurrencies))
}
is Result.Failure -> {}
}
}
}
@ -136,6 +136,7 @@ class DetailsMiddleware {
fun handle(action: DetailsAction.AppCurrencyAction) {
when (action) {
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
store.state.globalState.tapWalletManager.rates.clear()
preferencesStorage.saveAppCurrency(action.fiatCurrencyName)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName))
store.dispatch(WalletAction.LoadFiatRate())

View file

@ -43,13 +43,15 @@ private fun handlePrepareScreen(
action: DetailsAction.PrepareScreen,
state: DetailsState,
): DetailsState {
val backupIsActive = action.scanResponse.card.backupStatus?.isActive ?: false
return DetailsState(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = AppCurrencyState(action.fiatCurrencyName),
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName,
showAppCurrencyDialog = false,
),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
)

View file

@ -3,11 +3,11 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
import org.rekotlin.StateType
import java.util.*
@ -51,8 +51,9 @@ data class SecurityScreenState(
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
val fiatCurrencies: List<CurrenciesResponse.Currency>? = null,
)

View file

@ -257,7 +257,7 @@ class WalletConnectMiddleware {
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.derivationStyle)
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {

View file

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

View file

@ -336,7 +336,7 @@ class EmailDataBuilder(
appendLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
if (token.id != null) builder.appendKeyValue("Id", token.id ?: "")
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Contract address", token.contractAddress)
}
}

View file

@ -1,45 +1,68 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalUnitApi::class)
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AutoSizeText(
fun TextAutoSize(
modifier: Modifier = Modifier,
text: String,
textStyle: TextStyle,
modifier: Modifier = Modifier
textStyle: TextStyle = LocalTextStyle.current,
fontSizeRange: FontSizeRange,
) {
val scaledTextStyle = remember { mutableStateOf(textStyle) }
val fontSizeValue = remember { mutableStateOf(fontSizeRange.max.value) }
val readyToDraw = remember { mutableStateOf(false) }
val onTextLayout: (TextLayoutResult) -> Unit = { result ->
if (result.didOverflowWidth) {
val fontSize = result.size.width / result.multiParagraph.width
scaledTextStyle.value =
// scaledTextStyle.value.copy(fontSize = TextUnit(fontSize, TextUnitType.Sp))
scaledTextStyle.value.copy(fontSize = scaledTextStyle.value.fontSize * 0.9)
} else {
readyToDraw.value = true
}
val textState = remember { mutableStateOf(text) }
if (textState.value != text) {
readyToDraw.value = false
fontSizeValue.value = fontSizeRange.max.value
textState.value = text
}
Text(
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
text = text,
modifier = modifier.drawWithContent {
if (readyToDraw.value) {
drawContent()
}
},
softWrap = false,
onTextLayout = onTextLayout,
style = scaledTextStyle.value,
)
style = textStyle,
fontSize = fontSizeValue.value.sp,
onTextLayout = {
if (it.hasVisualOverflow) {
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
if (nextFontSizeValue <= fontSizeRange.min.value) {
fontSizeValue.value = fontSizeRange.min.value
readyToDraw.value = true
} else {
fontSizeValue.value = nextFontSizeValue * 0.8f
}
} else {
readyToDraw.value = true
}
}
)
}
data class FontSizeRange(
val min: TextUnit,
val max: TextUnit,
val step: TextUnit = DEFAULT_TEXT_STEP,
) {
init {
require(min < max) { "min should be less than max, $this" }
require(step.value > 0) { "step should be greater than 0, $this" }
}
companion object {
private val DEFAULT_TEXT_STEP = 1.sp
}
}

View file

@ -96,25 +96,33 @@ fun FirstStoriesContent(
)
if (screenState.value != StartingScreenState.MEET_TANGEM) {
AutoSizeText(text = text?.let { stringResource(text) } ?: "", textStyle = style,
TextAutoSize(
modifier = Modifier
.padding(start = 40.dp, end = 40.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),)
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.MEET_TANGEM,
enter = slideInVertically() { it / 2 }
) {
AutoSizeText(text = text?.let { stringResource(text) } ?: "", textStyle = style,
TextAutoSize(
modifier = Modifier
.padding(start = 40.dp, end = 40.dp)
.fillMaxWidth()
.padding(start = 40.dp, end = 40.dp),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.SHOW_CARD ||
screenState.value == StartingScreenState.MEET_TANGEM,
screenState.value == StartingScreenState.MEET_TANGEM,
enter = scaleIn(initialScale = 3f)
) {
Image(

View file

@ -32,7 +32,7 @@ class OnboardingManager(
suspend fun loadArtworkUrl(): String {
val cardInfo = cardInfo
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
this.cardInfo = cardInfo
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo)
}
@ -57,11 +57,11 @@ class OnboardingManager(
is Result.Failure -> {
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManagerUpdate.InternalError
is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManager.InternalError
else -> {
Timber.e(error.localizedMessage)
OnboardingWalletBalance.criticalError(TapError.WalletManagerUpdate.BlockchainIsUnreachableTryLater)
OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater)
}
}
}
@ -93,7 +93,7 @@ data class OnboardingWalletBalance(
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(

View file

@ -21,6 +21,7 @@ sealed class OnboardingWalletAction : Action {
sealed class BackupAction : Action {
object DetermineBackupStep : BackupAction()
object IntroduceBackup : BackupAction()
object StartBackup : BackupAction()
object DismissBackup : BackupAction()

View file

@ -111,7 +111,15 @@ private fun handleWalletAction(action: Action) {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> null
else -> {
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
walletState.backupState.backupStep == BackupStep.Finished
) {
BackupAction.IntroduceBackup
} else {
null
}
}
}
newAction?.let { store.dispatch(it) }
}

View file

@ -39,6 +39,10 @@ class BackupReducer {
): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
canSkipBackup = state.canSkipBackup,
)
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
BackupAction.StartAddingBackupCards -> {
state.copy(backupStep = BackupStep.AddBackupCards)
@ -104,6 +108,7 @@ class BackupReducer {
BackupAction.OnAccessCodeDialogClosed -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
BackupAction.DiscardBackup -> BackupState()
BackupAction.WritePrimaryCard -> state
is BackupAction.WriteBackupCard -> state
@ -117,7 +122,6 @@ class BackupReducer {
is BackupAction.CheckAccessCode -> state
BackupAction.DetermineBackupStep -> state
BackupAction.CheckForUnfinishedBackup -> state
BackupAction.DiscardBackup -> state
BackupAction.ResumeBackup -> state
BackupAction.DiscardSavedBackup -> state
BackupAction.StartBackup -> state

View file

@ -14,6 +14,7 @@ import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -57,6 +58,6 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment)
}
}
// addBackPressHandler(this)
addBackPressHandler(this)
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.features.tokens.addCustomToken
import android.content.Context
import com.tangem.domain.DomainError
import com.tangem.domain.ErrorConverter
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class DomainErrorConverter(
private val context: Context
) : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
val errorMessage = when (error) {
is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error)
else -> null
}
return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}"
}
}
private class AddCustomTokenConverter(
private val context: Context
) : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException()
val rawMessage = when (customTokenError) {
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
AddCustomTokenError.InvalidDecimalsCount -> R.string.custom_token_creation_error_wrong_decimals
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields
else -> null
}
return when (rawMessage) {
is Int -> context.getString(rawMessage)
// is String -> rawMessage
else -> "Unknown error: ${customTokenError::class.java.simpleName}"
}
}
}

View file

@ -12,21 +12,21 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.domain.ErrorConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.ToggledRippleTheme
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestAddCustomTokenActions
import com.tangem.wallet.R
/**
@ -52,7 +52,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
LazyColumn(
contentPadding = PaddingValues(bottom = 90.dp)
) {
// item { AddCustomTokenDebugActions() }
item { TestAddCustomTokenActions() }
item {
Surface(
modifier = Modifier.padding(16.dp),
@ -81,7 +81,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
@Composable
private fun FormFields(state: MutableState<AddCustomTokenState>) {
val context = LocalContext.current
val errorConverter = remember { DomainErrorConverter(context) }
val errorConverter = remember { ModuleMessageConverter(context) }
val stateValue = state.value
stateValue.form.fieldList.forEach { field ->
@ -98,32 +98,24 @@ private fun FormFields(state: MutableState<AddCustomTokenState>) {
}
@Composable
fun Warnings(warnings: List<AddCustomTokenWarning>) {
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
if (warnings.isEmpty()) return
val context = LocalContext.current
val warningConverter = remember { DomainErrorConverter(context) }
val warningConverter = remember { ModuleMessageConverter(context) }
Column {
warnings.forEachIndexed { index, item ->
val modifier = when (index) {
0 -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)
0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)
warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp)
else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp)
}
Surface(
AddCustomTokenWarning(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.warning_warning),
elevation = 4.dp,
) {
Text(
modifier = Modifier.padding(16.dp),
text = warningConverter.convertError(item),
color = colorResource(id = R.color.lightGray0),
fontSize = 14.sp
)
}
warning = item,
converter = warningConverter
)
}
}
}
@ -138,49 +130,53 @@ private fun AddButton(state: MutableState<AddCustomTokenState>) {
}
@Composable
fun AddCustomTokenFab(
private fun AddCustomTokenFab(
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
onClick: () -> Unit
) {
val contentColor = if (isEnabled) {
Color.White
val contentColor = Color.White
val backgroundColor = if (isEnabled) {
Color(0xFF1ACE80)
} else {
colorResource(id = R.color.darkGray1)
Color(0xFFB9E6D3)
}
val elevation = if (!isEnabled) {
FloatingActionButtonDefaults.elevation(0.dp, 0.dp)
} else {
FloatingActionButtonDefaults.elevation()
}
val backgroundColor = Color(0xFF1ACE80)
ExtendedFloatingActionButton(
modifier = modifier,
icon = {
Icon(
imageVector = Icons.Filled.Add,
tint = contentColor,
contentDescription = "Add",
)
},
text = {
Text(
text = stringResource(id = R.string.common_add),
)
},
onClick = onClick,
backgroundColor = backgroundColor,
contentColor = contentColor,
)
ToggledRippleTheme(isEnabled) {
ExtendedFloatingActionButton(
modifier = modifier,
icon = {
Icon(
imageVector = Icons.Filled.Add,
tint = contentColor,
contentDescription = "Add",
)
},
text = { Text(text = stringResource(id = R.string.common_add)) },
onClick = { if (isEnabled) onClick() },
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = elevation,
)
}
}
data class ScreenFieldData(
val field: DataField<*>,
val error: AddCustomTokenError?,
val errorConverter: ErrorConverter<String>,
val errorConverter: ModuleMessageConverter,
val viewState: ViewStates.TokenField
) {
companion object {
fun fromState(
field: DataField<*>,
state: AddCustomTokenState,
errorConverter: DomainErrorConverter
errorConverter: ModuleMessageConverter
): ScreenFieldData {
return ScreenFieldData(
field = field,

View file

@ -2,19 +2,19 @@ package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
import com.tangem.domain.features.addCustomToken.TokenField
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.BlockchainSpinner
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
import com.tangem.tap.common.compose.SpacerH8
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.tap.common.compose.*
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.wallet.R
/**
@ -27,19 +27,45 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
fieldData = tokenField.data,
labelId = R.string.custom_token_contract_address_input_title,
placeholder = "0x0000000000000000",
placeholder = "0x0000000000000000000000000000000000000000",
isEnabled = screenFieldData.viewState.isEnabled,
isLoading = screenFieldData.viewState.isLoading,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) }
) {
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it)))
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true)))
}
SpacerH8()
}
@Composable
private fun PasteClearButton(
showFirst: Boolean,
) {
if (showFirst) {
val context = LocalContext.current
PasteButton(
onClick = {
context.getFromClipboard()?.let {
val fieldData = Field.Data(it.toString(), false)
domainStore.dispatch(OnTokenContractAddressChanged(fieldData))
}
}
)
} else {
ClearButton(
onClick = {
val fieldData = Field.Data("", false)
domainStore.dispatch(OnTokenContractAddressChanged(fieldData))
}
)
}
}
@Composable
fun TokenNameView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
@ -47,14 +73,14 @@ fun TokenNameView(screenFieldData: ScreenFieldData) {
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
fieldData = tokenField.data,
labelId = R.string.custom_token_name_input_title,
placeholderId = R.string.custom_token_name_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it)))
domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true)))
}
SpacerH8()
}
@ -71,8 +97,8 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) }
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) }
SpacerH8()
}
@ -83,13 +109,13 @@ fun TokenSymbolView(screenFieldData: ScreenFieldData) {
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
fieldData = tokenField.data,
labelId = R.string.custom_token_token_symbol_input_title,
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) }
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) }
SpacerH8()
}
@ -100,14 +126,14 @@ fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
fieldData = tokenField.data,
labelId = R.string.custom_token_decimals_input_title,
placeholder = "8",
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) }
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) }
SpacerH8()
}
@ -123,12 +149,18 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
dropdownItemView = { blockchain ->
val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected)
val blockchainName = state.convertBlockchainName(blockchain, notSelected)
TitleSubtitle(derivationPathLabel, blockchainName)
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
TitleSubtitle(derivationPathName, blockchainName)
}
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) }
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) }
SpacerH8()
}
@Preview
@Composable
fun TestTokenContractAddressView() {
}

View file

@ -17,5 +17,5 @@ fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRe
items = dialog.items,
onSelect = dialog.onSelect,
onDismissRequest = onDismissRequest
) { contract -> TitleSubtitle(dialog.networkIdConverter(contract.networkId), contract.address) }
) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") }
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
package com.tangem.tap.features.tokens.addCustomToken.compose.test
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
@ -12,21 +12,17 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.common.services.Result
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.TangemTechServiceManager
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
import com.tangem.domain.redux.domainStore
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.wallet.BuildConfig
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Composable
fun AddCustomTokenDebugActions() {
if (!BuildConfig.DEBUG) return
fun TestAddCustomTokenActions() {
if (!BuildConfig.TEST_ACTION_ENABLED) return
Column() {
// deep test
@ -40,27 +36,22 @@ fun AddCustomTokenDebugActions() {
ActionRow("All in one") { AllInOne() }
// Any action
// ActionRow("CustomActions - find tokens active=false, decimals != null") { CustomActions() }
// ActionRow("CustomActions - find coins active=false, decimals != null") { CustomActions() }
}
}
@Composable
private fun AllInOne() {
// validation error
ContractAddressButton(
name = "invalid",
address = "unk"
)
// ContractAddressButton(
// name = "invalid",
// address = "unk"
// )
// active = true
ContractAddressButton(
name = "true",
address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
)
// active = false, decimalCount != null
ContractAddressButton(
name = "false",
address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f"
)
// more than one network
ContractAddressButton(
name = ">1 network",
@ -68,9 +59,17 @@ private fun AllInOne() {
)
// unknown
ContractAddressButton(
name = "unk",
name = "unknown",
address = "0x1111111111111111112111111111111111111113"
)
ContractAddressButton(
name = "full custom",
address = "0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1",
tokenNetwork = Blockchain.Ethereum,
tokenName = "Test unknown",
tokenSymbol = "TU",
tokenDecimals = "5",
)
}
@Composable
@ -143,41 +142,6 @@ private fun UnknownContracts() {
@Composable
private fun CustomActions() {
CustomActionButton(
name = "Find tokens in several networks",
action = {
val manager = TangemTechServiceManager(TangemTechService())
val currencies = manager.tokens()
val asdfsd = mutableMapOf<String, MutableList<Any>>()
val contractAddresses = currencies.mapNotNull { currency ->
currency.contracts?.map { it.address }
}.flatten()
contractAddresses.take(500).forEachIndexed() { index, address ->
when (val result = manager.checkAddress(address)) {
is Result.Success -> {
val contractList = mutableListOf<Any>()
result.data.forEach { token ->
token.contracts.forEach { contract ->
if (!contract.active && contract.decimalCount != null) {
contractList.add(contract)
}
}
}
if (contractList.isNotEmpty()) {
val list = asdfsd[address] ?: mutableListOf()
list.addAll(contractList)
asdfsd[address] = list
}
Timber.e("Success. handle $index item from size ${contractAddresses.size}. Result = ${asdfsd.size}")
}
is Result.Failure -> {}
}
}
val result = asdfsd.filter { it.value.size > 1 }
if (result.isEmpty()) return@CustomActionButton
}
)
}
@Composable
@ -214,10 +178,26 @@ private fun ActionButton(
private fun ContractAddressButton(
name: String,
address: String,
tokenNetwork: Blockchain? = null,
tokenName: String? = null,
tokenSymbol: String? = null,
tokenDecimals: String? = null,
) {
ActionButton(name = name) {
resetTokenValues()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false)))
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(address, false)))
tokenNetwork?.let {
domainStore.dispatch(OnTokenNetworkChanged(Field.Data(tokenNetwork, false)))
}
tokenName?.let {
domainStore.dispatch(OnTokenNameChanged(Field.Data(tokenName, false)))
}
tokenSymbol?.let {
domainStore.dispatch(OnTokenSymbolChanged(Field.Data(tokenSymbol, false)))
}
tokenDecimals?.let {
domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(tokenDecimals, false)))
}
}
}
@ -238,7 +218,7 @@ private fun CustomActionButton(
}
private fun resetTokenValues() {
domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
domainStore.dispatch(OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
// domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false)))
// domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false)))
// domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false)))

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
@ -13,13 +13,12 @@ import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.features.addCustomToken.CompleteData
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -28,19 +27,16 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
class TokensMiddleware {
val tokensMiddleware: Middleware<AppState> = { dispatch, state ->
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
@ -58,8 +54,10 @@ class TokensMiddleware {
val isTestcard = scanResponse?.card?.isTestCard ?: false
scope.launch {
val currencies = async { currenciesRepository.getSupportedTokens(isTestcard)
.filter(action.supportedBlockchains?.toSet()) }
val currencies = async {
currenciesRepository.getSupportedTokens(isTestcard)
.filter(action.supportedBlockchains?.toSet())
}
val delay = async { delay(600) }
delay.await()
store.dispatchOnMain(TokensAction.LoadCurrencies.Success(currencies.await()))
@ -69,58 +67,80 @@ class TokensMiddleware {
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
val scanResponse = store.state.globalState.scanResponse ?: return
val currentTokens = store.state.tokensState.addedWallets.toTokens()
val currentBlockchains = store.state.tokensState.addedWallets.toBlockchains(
store.state.tokensState.derivationStyle
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
scanResponse.card.derivationStyle
)
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
scanResponse.card.derivationStyle
)
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
val blockchainsToRemove = currentBlockchains.filter { !action.addedBlockchains.contains(it) }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it.token) }
val tokensToRemove = currentTokens.filter { token -> !action.addedTokens.any { it.token == token } }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
val tokensToRemove = currentTokens.filter {
token -> !action.addedTokens.any { it.token == token.token }
}
val derivationStyle = scanResponse.card.derivationStyle
removeCurrenciesIfNeeded(blockchainsToRemove, tokensToRemove)
removeCurrenciesIfNeeded(convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
derivationStyle = derivationStyle
))
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
store.dispatchDebugErrorNotification("Nothing to save")
store.dispatch(NavigationAction.PopBackTo())
return
}
val currencyList = convertToCurrencies(
blockchains = blockchainsToAdd,
tokens = tokensToAdd,
derivationStyle = derivationStyle
)
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, blockchainsToAdd, tokensToAdd)
deriveMissingBlockchains(scanResponse, currencyList) {
submitAdd(it, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitAdd(blockchainsToAdd, tokensToAdd, scanResponse)
store.dispatch(NavigationAction.PopBackTo())
submitAdd(scanResponse, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
val tokensState = store.state.tokensState
val addedTokensList = tokensState.addedTokens.map {
DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain)
private fun convertToCurrencies(
blockchains: List<Blockchain>,
tokens: List<TokenWithBlockchain>,
derivationStyle: DerivationStyle?
): List<Currency> {
return blockchains.map {
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
} + tokens.map {
Currency.Token(
it.token,
it.blockchain,
it.blockchain.derivationPath(derivationStyle)?.rawPath
)
}
val addedBlockchains = tokensState.addedBlockchains.map { it }
val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains)
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
val callback = fun(data: CompleteData) {
Timber.e("Yoooohhhoooo")
}
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback))
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
blockchains: List<Blockchain>,
tokens: List<TokenWithBlockchain>
currencyList: List<Currency>,
onSuccess: (ScanResponse) -> Unit,
) {
val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, blockchains, tokens),
getDerivations(EllipticCurve.Ed25519, scanResponse, blockchains, tokens)
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList)
)
val derivations = derivationDataList.map { it.derivations }.toMap()
val derivations = derivationDataList.associate { it.derivations }
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
@ -145,13 +165,11 @@ class TokensMiddleware {
derivedKeys = updatedDerivedKeys
)
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
submitAdd(blockchains, tokens, updatedScanResponse)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.PopBackTo())
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchErrorNotification(TapError.CustomError("Error adding tokens"))
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
@ -160,20 +178,31 @@ class TokensMiddleware {
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
blockchains: List<Blockchain>,
tokens: List<TokenWithBlockchain>
currencyList: List<Currency>,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val derivationPathsCandidates = (blockchains + tokens.map { it.blockchain }).distinct()
.mapNotNull { it.derivationPath(scanResponse.card.derivationStyle) }
val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter {
it.getSupportedCurves().contains(curve)
}.mapNotNull {
it.derivationPath(scanResponse.card.derivationStyle)
}
val customTokensCandidates = currencyList.filter {
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
if (bothCandidates.isEmpty()) return null
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) }
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
alreadyDerivedKeys = alreadyDerivedKeys,
@ -188,37 +217,93 @@ class TokensMiddleware {
)
private fun submitAdd(
blockchains: List<Blockchain>, tokens: List<TokenWithBlockchain>, scanResponse: ScanResponse,
scanResponse: ScanResponse,
currencyList: List<Currency>,
) {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val derivationStyle = scanResponse.card.derivationStyle
(blockchains.mapNotNull {
val walletManager = factory.makeWalletManagerForApp(
scanResponse, it,
scanResponse.card.derivationStyle?.let { DerivationParams.Default(it) }
) ?: return@mapNotNull null
WalletAction.MultiWallet.AddBlockchain(BlockchainNetwork.fromWalletManager(walletManager), walletManager)
} + tokens.map {
val blockchainNetwork = BlockchainNetwork(it.blockchain, scanResponse.card)
WalletAction.MultiWallet.AddToken(it.token, blockchainNetwork)
}).forEach { store.dispatchOnMain(it) }
}
val addActions = currencyList.mapNotNull { currency ->
when (currency) {
is Currency.Blockchain -> {
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
private fun removeCurrenciesIfNeeded(blockchains: List<Blockchain>, tokens: List<Token>) {
if (tokens.isNotEmpty()) {
tokens.forEach { token ->
store.state.walletState.getWalletData(token)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
val derivationParams = derivationStyle?.let {
when (derivationPath) {
null -> DerivationParams.Default(derivationStyle)
else -> DerivationParams.Custom(derivationPath)
}
}
val walletManager = factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = currency.blockchain,
derivationParams = derivationParams
) ?: return@mapNotNull null
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, walletManager)
}
is Currency.Token -> {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork = BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork)
}
}
}
if (blockchains.isNotEmpty()) {
blockchains.forEach { blockchain ->
store.state.walletState.getWalletData(blockchain)?.let {
addActions.forEach { store.dispatchOnMain(it) }
}
private fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
if (currencies.isNotEmpty()) {
currencies.forEach { currency ->
store.state.walletState.getWalletData(currency)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
}
}
}
}
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
return currency.derivationPath?.let {
!scanResponse.hasDerivation(currency.blockchain, it)
} ?: false
}
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
val scanResponse = store.state.globalState.scanResponse ?: return
fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List<Currency>) {
submitAdd(scanResponse, currencyList)
// pop from the AddCustomTokenScreen
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchOnMain(NavigationAction.PopBackTo())
}
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
val currencyList = listOf(currency)
if (isNeedToDerive) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitAndPopBack(it, currencyList)
}
} else {
submitAndPopBack(scanResponse, currencyList)
}
}
val addedCurrencies = store.state.walletState.wallets.map { walletStore ->
walletStore.walletsData.map { walletData -> walletData.currency }
}.flatten().map {
when (it) {
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(it.blockchain, it.derivationPath)
is Currency.Token -> DomainWrapped.Currency.Token(it.token, it.blockchain, it.derivationPath)
}
}
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
}
}

View file

@ -22,15 +22,15 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(
addedBlockchains = action.wallets.toBlockchains(action.derivationStyle),
addedTokens = action.wallets.toTokensWithBlockchains(action.derivationStyle),
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
addedWallets = action.wallets,
derivationStyle = action.derivationStyle
)
}
is TokensAction.SetNonRemovableCurrencies -> {
tokensState.copy(
nonRemovableBlockchains = action.wallets.toBlockchains(tokensState.derivationStyle),
nonRemovableBlockchains = action.wallets.toNonCustomBlockchains(tokensState.derivationStyle),
nonRemovableTokens = action.wallets.toTokensContractAddresses(),
)
}

View file

@ -27,11 +27,13 @@ fun List<WalletData>.toTokensContractAddresses(): List<ContractAddress> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token?.contractAddress }.distinct()
}
fun List<WalletData>.toTokens(): List<Token> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }.distinct()
fun List<WalletData>.toNonCustomTokens(derivationStyle: DerivationStyle?): List<Token> {
return filter { !it.currency.isCustomCurrency(derivationStyle) }
.mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }
.distinct()
}
fun List<WalletData>.toTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
fun List<WalletData>.toNonCustomTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
return mapNotNull {
if (it.currency !is com.tangem.tap.features.wallet.redux.Currency.Token) return@mapNotNull null
if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
@ -39,7 +41,7 @@ fun List<WalletData>.toTokensWithBlockchains(derivationStyle: DerivationStyle?):
}.distinct()
}
fun List<WalletData>.toBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
fun List<WalletData>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return mapNotNull {
if (it.currency.isCustomCurrency(derivationStyle)) {
null
@ -58,8 +60,9 @@ fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency
if (supportedBlockchains == null) return this
return map {
it.copy(contracts =
it.contracts?.filter {
supportedBlockchains.contains(it.blockchain) && it.blockchain.canHandleTokens()
it.contracts.filter {
supportedBlockchains.contains(it.blockchain) &&
(it.blockchain.canHandleTokens() || it.address == null)
}
)
}.filterNot {

View file

@ -115,6 +115,9 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.popular_tokens, menu)
val addCustomTokenAllowed = store.state.tokensState.allowToAdd
menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed
val menuItem = menu.findItem(R.id.menu_search)
val searchView: SearchView = menuItem.actionView as SearchView
searchView.queryHint = searchView.getString(R.string.add_token_search_hint)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@ -10,11 +11,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.extensions.getGreyedOutIconRes
@ -36,10 +39,12 @@ fun CollapsedCurrencyItem(
.clickable(onClick = { onCurrencyClick(currency.id) })
) {
val blockchain = Blockchain.fromNetworkId(currency.id)
val iconRes = currency.iconUrl
SubcomposeAsyncImage(
model = iconRes,
model = ImageRequest.Builder(LocalContext.current)
.data(currency.iconUrl)
.crossfade(true)
.build(),
contentDescription = currency.id,
loading = { CurrencyPlaceholderIcon(currency.id) },
error = { CurrencyPlaceholderIcon(currency.id) },
@ -55,7 +60,7 @@ fun CollapsedCurrencyItem(
.align(Alignment.CenterVertically)
) {
Text(
text = currency.name,
text = currency.fullName,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),
@ -64,9 +69,10 @@ fun CollapsedCurrencyItem(
Row {
if (!currency.contracts.isNullOrEmpty()) {
currency.contracts.map { contract ->
if (contract.address == currency.symbol) {
if (contract.address == null) {
BlockchainNetworkItem(
blockchain = Blockchain.fromNetworkId(contract.networkId),
blockchain = contract.blockchain,
isMainNetwork = true,
addedBlockchains = addedBlockchains
)
} else {
@ -91,6 +97,7 @@ fun CollapsedCurrencyItem(
} else {
BlockchainNetworkItem(
blockchain = blockchain,
isMainNetwork = false,
addedBlockchains = addedBlockchains
)
}
@ -110,18 +117,39 @@ fun CollapsedCurrencyItem(
@Composable
fun BlockchainNetworkItem(
blockchain: Blockchain?,
isMainNetwork: Boolean,
addedBlockchains: List<Blockchain>
) {
val added = addedBlockchains.contains(blockchain)
val icon =
if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes()
if (icon != null) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
Box(Modifier
.size(20.dp)) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
if (isMainNetwork) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(7.dp)
.clip(CircleShape)
.background(Color.White)
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(5.dp)
.clip(CircleShape)
.background(Color(0xFF1ACE80))
)
}
}
}
Spacer(modifier = Modifier.size(5.dp))
}
}

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
@ -126,7 +127,7 @@ fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
)
},
onClick = onSaveChanges,
backgroundColor = Color(0xFF1ACE80),
backgroundColor = colorResource(id = R.color.accent),
contentColor = Color.White,
modifier = Modifier.padding(bottom = padding.dp)
)

View file

@ -11,14 +11,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@ -36,9 +37,6 @@ fun ExpandedCurrencyItem(
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
val blockchain = Blockchain.fromNetworkId(currency.id)
val iconRes = currency.iconUrl
Column {
Row(
modifier = Modifier
@ -46,7 +44,10 @@ fun ExpandedCurrencyItem(
.clickable(onClick = { onCurrencyClick(currency.id) })
) {
SubcomposeAsyncImage(
model = iconRes,
model = ImageRequest.Builder(LocalContext.current)
.data(currency.iconUrl)
.crossfade(true)
.build(),
contentDescription = currency.id,
loading = { CurrencyPlaceholderIcon(currency.id) },
error = { CurrencyPlaceholderIcon(currency.id) },
@ -63,7 +64,7 @@ fun ExpandedCurrencyItem(
.align(Alignment.CenterVertically)
) {
Text(
text = currency.name,
text = currency.fullName,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),
@ -85,9 +86,7 @@ fun ExpandedCurrencyItem(
)
}
val blockchains = currency.contracts?.map { it.blockchain } ?: listOfNotNull(
Blockchain.fromNetworkId(currency.id)
)
val blockchains = currency.contracts.map { it.blockchain }
Row {
Box(
@ -132,13 +131,14 @@ fun ExpandedCurrencyItem(
.padding(top = 6.dp),
) {
blockchains.map { blockchain ->
val contract = currency.contracts?.firstOrNull { it.blockchain == blockchain }
val added = if (contract != null && contract.address != currency.symbol) {
val contract = currency.contracts.firstOrNull { it.blockchain == blockchain }
?: return@map
val added = if (contract.address != null) {
addedTokens.map { it.token.contractAddress }.contains(contract.address)
} else {
addedBlockchains.contains(blockchain)
}
val canBeRemoved = if (contract != null && contract.address != currency.symbol) {
val canBeRemoved = if (contract.address != null) {
!nonRemovableTokens.contains(contract.address)
} else {
!nonRemovableBlockchains.contains(blockchain)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -7,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
@ -39,7 +41,9 @@ fun ListOfCurrencies(
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(bottom = 90.dp)
) {
val filteredCurrencies = if (searchInput.isBlank()) {
@ -66,4 +70,7 @@ fun ListOfCurrencies(
}
}
}
}
val Currency.fullName: String
get() = "${this.name} (${this.symbol})"

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
@ -14,12 +15,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.common.extensions.getRoundIconRes
@ -31,39 +35,57 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NetworkItem(
currency: Currency, contract: Contract?,
currency: Currency, contract: Contract,
blockchain: Blockchain, allowToAdd: Boolean,
added: Boolean, canBeRemoved: Boolean,
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
val isBlockchain = contract == null || contract.address == currency.symbol
Row(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = allowToAdd,
onLongClick = {
if (contract != null) onNetworkItemClicked(contract.address)
contract.address?.let { onNetworkItemClicked(it) }
},
onClick = {},
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
) {
SubcomposeAsyncImage(
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
contentDescription = blockchain.fullName,
loading = { CurrencyPlaceholderIcon(blockchain.id) },
error = { CurrencyPlaceholderIcon(blockchain.id) },
Box(
modifier = Modifier
.clip(CircleShape)
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
.size(20.dp)
.align(Alignment.CenterVertically)
)
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
) {
SubcomposeAsyncImage(
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
contentDescription = blockchain.fullName,
loading = { CurrencyPlaceholderIcon(blockchain.id) },
error = { CurrencyPlaceholderIcon(blockchain.id) },
modifier = Modifier
.size(20.dp)
)
if (contract.address == null) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(7.dp)
.clip(CircleShape)
.background(Color.White)
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(5.dp)
.clip(CircleShape)
.background(Color(0xFF1ACE80))
)
}
}
}
Row(
modifier = Modifier
.fillMaxHeight()
@ -71,36 +93,32 @@ fun NetworkItem(
.align(Alignment.CenterVertically)
) {
Text(
text = blockchain.name.uppercase(),
fontSize = 13.sp,
text = prepareNetworkNameSpannableText(
blockchain = blockchain,
contractAddress = contract.address
),
fontWeight = FontWeight.SemiBold,
color = if (added) Color.Black else Color(0xFF848488),
)
Spacer(modifier = Modifier.size(3.dp))
Text(
text = if (isBlockchain) "MAIN" else blockchain.getNetworkName().uppercase(),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (!isBlockchain) Color(0xFF8E8E93) else Color(0xFF1ACE80),
color = if (added) Color.Black else Color(0xFF848488),
)
}
if (allowToAdd) {
val token = if (!isBlockchain) {
val token = if (contract.address != null) {
Token(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
contractAddress = contract!!.address,
decimals = contract.decimalCount,
contractAddress = contract.address,
decimals = contract.decimalCount!!,
)
} else {
null
}
val tokenWithBlockchain =
token?.let { TokenWithBlockchain(token, contract!!.blockchain) }
token?.let { TokenWithBlockchain(token, contract.blockchain) }
val currencyToSave = if (isBlockchain && contract != null) {
val currencyToSave = if (contract.address == null) {
currency.copy(id = contract.networkId)
} else {
currency
@ -117,4 +135,33 @@ fun NetworkItem(
)
}
}
}
@Composable
fun prepareNetworkNameSpannableText(
blockchain: Blockchain,
contractAddress: String?
): AnnotatedString {
val blockchainName = blockchain.fullNameWithoutTestnet.uppercase()
val additionalText =
if (contractAddress == null) "MAIN" else blockchain.getNetworkName().uppercase()
val text = "$blockchainName $additionalText"
val startOfAdditionalText =
if (additionalText.isNotBlank()) text.indexOf(additionalText) else text.length
val spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(
fontWeight = FontWeight.Normal,
color = if (contractAddress != null) Color(0xFF8E8E93) else Color(0xFF1ACE80)
),
start = startOfAdditionalText,
end = text.length
)
)
return AnnotatedString(text = text, spanStyles = spanStyles)
}

View file

@ -58,8 +58,8 @@ sealed class WalletAction : Action {
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
data class SaveCurrencies(val blockchainNetworks: List<BlockchainNetwork>) : MultiWallet()
object FindTokensInUse : MultiWallet()
object FindBlockchainsInUse : MultiWallet()
// object FindTokensInUse : MultiWallet()
// object FindBlockchainsInUse : MultiWallet()
data class TokenLoaded(
val amount: Amount,
@ -91,10 +91,12 @@ sealed class WalletAction : Action {
}
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
object RestoreFundsWarningClosed : Warnings()
}
data class LoadFiatRate(
val wallet: Wallet? = null, val currencyList: List<Currency>? = null,
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
) : WalletAction() {
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
object Failure : WalletAction()
@ -159,4 +161,6 @@ sealed class WalletAction : Action {
val minRent: String,
val rentExempt: String
) : WalletAction()
data class RemoveWalletRent(val blockchain: BlockchainNetwork) : WalletAction()
}

View file

@ -5,7 +5,8 @@ import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.StateDialog
@ -17,6 +18,7 @@ import com.tangem.tap.domain.extensions.sellIsAllowed
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
@ -59,7 +61,7 @@ data class WalletState(
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val blockchains: List<Blockchain>
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
@ -73,19 +75,9 @@ data class WalletState(
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
fun getWalletManager(token: Token?): WalletManager? {
if (token == null) return null
return wallets
.mapNotNull { it.walletManager }
.find { walletManager ->
walletManager.cardTokens.any { it.contractAddress == token.contractAddress }
}
}
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return wallets.map { it.walletManager }
.find { it?.wallet?.blockchain == currency.blockchain }
return getWalletStore(currency)?.walletManager
}
fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? {
@ -96,8 +88,8 @@ data class WalletState(
if (blockchain == null) return null
return walletsData.find {
it.currency is Currency.Blockchain &&
it.currency.blockchain == blockchain.blockchain &&
it.currency.derivationPath == blockchain.derivationPath
it.currency.blockchain == blockchain.blockchain &&
it.currency.derivationPath == blockchain.derivationPath
}
}
@ -105,7 +97,7 @@ data class WalletState(
if (currency == null) return null
return wallets.firstOrNull {
it.blockchainNetwork.derivationPath == currency.derivationPath &&
(it.blockchainNetwork.blockchain == currency.blockchain)
(it.blockchainNetwork.blockchain == currency.blockchain)
}
}
@ -120,7 +112,7 @@ data class WalletState(
if (blockchainNetwork == null) return null
return wallets.firstOrNull {
it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath &&
(it.blockchainNetwork.blockchain == blockchainNetwork.blockchain)
(it.blockchainNetwork.blockchain == blockchainNetwork.blockchain)
}
}
@ -129,22 +121,6 @@ data class WalletState(
return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency }
}
fun getWalletData(token: Token?): WalletData? {
if (token == null) return null
return walletsData.find {
(it.currency as? Currency.Token)?.token == token &&
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
}
}
fun getWalletData(blockchain: Blockchain?): WalletData? {
if (blockchain == null) return null
return walletsData.find {
(it.currency as? Currency.Blockchain)?.blockchain == blockchain &&
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
}
}
fun getSelectedWalletData(): WalletData? {
return walletsData.find { it.currency == selectedCurrency }
}
@ -166,23 +142,23 @@ data class WalletState(
if (walletData.currency is Currency.Blockchain) {
return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() &&
wallet.amounts.toSendableAmounts().isEmpty()
wallet.amounts.toSendableAmounts().isEmpty()
} else if (walletData.currency is Currency.Token) (
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address
).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
)
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address
).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
)
}
return false
}
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
return (walletData.currency is Currency.Blockchain &&
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|| (walletData.currency is Currency.Token &&
walletData.currency.token == store.state.walletState.primaryToken)
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|| (walletData.currency is Currency.Token &&
walletData.currency.token == store.state.walletState.primaryToken)
}
fun replaceWalletInWallets(wallet: WalletStore?): List<WalletStore> {
@ -239,7 +215,7 @@ data class WalletState(
return if (walletData.currency is Currency.Blockchain) {
val walletStores = wallets.filterNot {
it.blockchainNetwork.blockchain == walletData.currency.blockchain
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
}
copy(wallets = walletStores)
} else {
@ -396,6 +372,11 @@ data class WalletRent(
sealed interface Currency {
val coinId: String?
get() = when (this) {
is Blockchain -> blockchain.toCoinId()
is Token -> token.id
}
val blockchain: com.tangem.blockchain.common.Blockchain
val currencySymbol: CryptoCurrencyName
val derivationPath: String?
@ -416,10 +397,16 @@ sealed interface Currency {
}
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
if (this is Token && this.token.id == null) return true
if (derivationPath == null || derivationStyle == null) return false
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
}
fun isBlockchain(): Boolean = this is Blockchain
fun isToken(): Boolean = this is Token
companion object {
fun fromBlockchainNetwork(
@ -427,18 +414,40 @@ sealed interface Currency {
token: com.tangem.blockchain.common.Token? = null
): Currency {
return if (token != null) {
Currency.Token(
Token(
token = token,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
)
} else {
Currency.Blockchain(
Blockchain(
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
)
}
}
fun fromCustomCurrency(customCurrency: CustomCurrency): Currency {
return when (customCurrency) {
is CustomCurrency.CustomBlockchain -> Blockchain(
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath
)
is CustomCurrency.CustomToken -> Token(
token = customCurrency.token,
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
}
}
fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token {
return Token(
token = tokenWithBlockchain.token,
blockchain = tokenWithBlockchain.blockchain,
derivationPath = null
)
}
}
}
@ -450,7 +459,7 @@ data class WalletStore(
fun updateWallets(walletDataList: List<WalletData>): WalletStore {
val relevantWalletDataList = walletDataList.filter {
it.currency.blockchain == blockchainNetwork.blockchain &&
it.currency.derivationPath == blockchainNetwork.derivationPath
it.currency.derivationPath == blockchainNetwork.derivationPath
}.toMutableList()
val updatedWalletDataList = walletsData.map { walletData ->
val matchingWalletData = relevantWalletDataList.find { it.currency == walletData.currency }

View file

@ -1,15 +1,14 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
@ -19,7 +18,6 @@ import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@ -29,7 +27,7 @@ class MultiWalletMiddleware {
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?,
) {
val globalState = globalState ?: return
val tapWalletManager = globalState.tapWalletManager
// val tapWalletManager = globalState.tapWalletManager
when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
@ -57,19 +55,17 @@ class MultiWalletMiddleware {
blockchainNetwork = action.blockchain
)
}
store.dispatch(
WalletAction.LoadFiatRate(
currencyList = listOf(
Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
)
store.dispatch(WalletAction.LoadFiatRate(
coinsList = listOf(
Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
)
)
)
store.dispatch(
WalletAction.LoadWallet(action.blockchain, action.walletManager)
)
))
store.dispatch(WalletAction.LoadWallet(
action.blockchain, action.walletManager
))
}
is WalletAction.MultiWallet.SaveCurrencies -> {
globalState.scanResponse?.card?.cardId?.let {
@ -91,7 +87,7 @@ class MultiWalletMiddleware {
}
}
is Currency.Token -> {
val walletManager = walletState?.getWalletManager(currency.token)
val walletManager = walletState?.getWalletManager(currency)
if (walletManager != null) {
walletManager.removeToken(currency.token)
cardId?.let {
@ -105,98 +101,94 @@ class MultiWalletMiddleware {
}
}
}
is WalletAction.MultiWallet.FindBlockchainsInUse -> {
val scanResponse = globalState.scanResponse ?: return
if (scanResponse.supportsHdWallet()) return
val cardFirmware = scanResponse.card.firmwareVersion
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
.filterNot { walletState?.blockchains?.contains(it) == true }
.map { BlockchainNetwork(it, null, emptyList()) }
val walletManagers =
tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
scanResponse,
blockchains
)
scope.launch {
walletManagers.map { walletManager ->
async(Dispatchers.IO) {
walletManager.safeUpdate()
val wallet = walletManager.wallet
val coinAmount = wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
scope.launch(Dispatchers.Main) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
if (walletState?.getWalletData(blockchainNetwork) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
blockchainNetwork, walletManager
)
)
store.dispatch(
WalletAction.LoadWallet.Success(
wallet = wallet,
blockchain = blockchainNetwork
)
)
}
}
}
}
}
}
}
is WalletAction.MultiWallet.FindTokensInUse -> {
val scanResponse = globalState.scanResponse ?: return
if (scanResponse.supportsHdWallet()) return
val walletFactory = tapWalletManager.walletManagerFactory
val card = scanResponse.card
val walletManager = walletState?.getWalletManager(
Currency.Blockchain(Blockchain.Ethereum, null)
)
?: walletFactory.makeWalletManagerForApp(
scanResponse,
Currency.Blockchain(Blockchain.Ethereum, null)
)
val tokenFinder = walletManager as? TokenFinder ?: return
scope.launch {
val result = tokenFinder.findTokens()
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
if (result.data.isNotEmpty()) {
val blockchainNetwork = BlockchainNetwork(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
walletManager.cardTokens.toList()
)
currenciesRepository.saveUpdatedCurrency(
card.cardId,
blockchainNetwork
)
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
blockchainNetwork,
walletManager
)
)
store.dispatch(
WalletAction.MultiWallet.AddTokens(
walletManager.cardTokens.toList(),
blockchainNetwork
)
)
}
}
}
}
}
}
// is WalletAction.MultiWallet.FindBlockchainsInUse -> {
// val scanResponse = globalState.scanResponse ?: return
// if (scanResponse.supportsHdWallet()) return
//
// val cardFirmware = scanResponse.card.firmwareVersion
// val blockchains = currenciesRepository.getBlockchains(cardFirmware)
// .filterNot { walletState?.blockchains?.contains(it) == true }
// .map { BlockchainNetwork(it, null, emptyList()) }
// val walletManagers =
// tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
// scanResponse,
// blockchains
// )
//
// scope.launch {
// walletManagers.map { walletManager ->
// async(Dispatchers.IO) {
// walletManager.safeUpdate()
// val wallet = walletManager.wallet
// val coinAmount = wallet.amounts[AmountType.Coin]?.value
// if (coinAmount != null && !coinAmount.isZero()) {
// scope.launch(Dispatchers.Main) {
// val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
// if (walletState?.getWalletData(blockchainNetwork) == null) {
// store.dispatch(WalletAction.MultiWallet.AddBlockchain(
// blockchainNetwork, walletManager
// ))
// store.dispatch(WalletAction.LoadWallet.Success(
// wallet = wallet,
// blockchain = blockchainNetwork
// ))
// }
// }
// }
// }
// }
// }
// }
// is WalletAction.MultiWallet.FindTokensInUse -> {
// val scanResponse = globalState.scanResponse ?: return
// if (scanResponse.supportsHdWallet()) return
//
// val walletFactory = tapWalletManager.walletManagerFactory
// val card = scanResponse.card
//
// val walletManager = walletState?.getWalletManager(
// Currency.Blockchain(Blockchain.Ethereum, null)
// )
// ?: walletFactory.makeWalletManagerForApp(
// scanResponse,
// Currency.Blockchain(Blockchain.Ethereum, null)
// )
//
// val tokenFinder = walletManager as? TokenFinder ?: return
// scope.launch {
// val result = tokenFinder.findTokens()
//
// withContext(Dispatchers.Main) {
// when (result) {
// is Result.Success -> {
// if (result.data.isNotEmpty()) {
// val blockchainNetwork = BlockchainNetwork(
// walletManager.wallet.blockchain,
// walletManager.wallet.publicKey.derivationPath?.rawPath,
// walletManager.cardTokens.toList()
// )
// currenciesRepository.saveUpdatedCurrency(
// card.cardId,
// blockchainNetwork
// )
// store.dispatch(
// WalletAction.MultiWallet.AddBlockchain(
// blockchainNetwork,
// walletManager
// )
// )
// store.dispatch(
// WalletAction.MultiWallet.AddTokens(
// walletManager.cardTokens.toList(),
// blockchainNetwork
// )
// )
// }
// }
// }
// }
// }
// }
}
}
@ -222,6 +214,7 @@ class MultiWalletMiddleware {
tokens: List<Token>, blockchainNetwork: BlockchainNetwork,
walletState: WalletState?, globalState: GlobalState?
) {
if (tokens.isEmpty()) return
val scanResponse = globalState?.scanResponse ?: return
val wmFactory = globalState.tapWalletManager.walletManagerFactory
@ -230,12 +223,13 @@ class MultiWalletMiddleware {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, it))
} ?: return
store.dispatch(WalletAction.LoadFiatRate(currencyList = tokens.map { token ->
store.dispatch(WalletAction.LoadFiatRate(coinsList = tokens.map { token ->
Currency.Token(
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
)
}))
walletManager.addTokens(tokens)
if (tokens.isNotEmpty()) walletManager.addTokens(tokens)
currenciesRepository.saveUpdatedCurrency(
cardId = scanResponse.card.cardId,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)

View file

@ -50,7 +50,7 @@ class TradeCryptoMiddleware {
if (exchangeAction == CurrencyExchangeManager.Action.Buy &&
currency is Currency.Token && currency.blockchain.isTestnet()
) {
val walletManager = store.state.walletState.getWalletManager(currency.token)
val walletManager = store.state.walletState.getWalletManager(currency)
if (walletManager !is EthereumWalletManager) return
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }

View file

@ -3,8 +3,10 @@ package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.isZero
@ -20,9 +22,14 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.scope
@ -34,6 +41,8 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import timber.log.Timber
import java.math.BigDecimal
class WalletMiddleware {
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
@ -77,49 +86,52 @@ class WalletMiddleware {
}
}
is WalletAction.LoadWallet.Success -> {
checkForRentWarning(walletState.getWalletManager(action.blockchain))
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState.getWalletData(action.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
)
)
store.dispatch(
WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain
)
)
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
))
store.dispatch(WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain
))
}
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.LoadFiatRate -> {
val tapWalletManager = globalState.tapWalletManager
val fiatAppCurrency = globalState.appCurrency
val appCurrencyId = globalState.appCurrency
scope.launch {
when {
val coinsList = when {
action.wallet != null -> {
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = fiatAppCurrency,
wallet = action.wallet,
)
val wallet = action.wallet
wallet.getTokens()
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
}
action.currencyList != null -> {
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = fiatAppCurrency,
currencies = action.currencyList,
)
action.coinsList != null -> action.coinsList
else -> walletState.walletsData.map { it.currency }
}
val ratesResult = globalState.tapWalletManager.rates.loadFiatRate(
currencyId = appCurrencyId,
coinsList = coinsList,
)
when (ratesResult) {
is Result.Success -> {
ratesResult.data.loadedRates.forEach {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it.toPair()))
}
ratesResult.data.failedRates.forEach { (currency, throwable) ->
Timber.e(throwable, "Loading rates failed for [%s]", currency.currencySymbol)
}
}
else -> {
val currencyList = walletState.walletsData.map { it.currency }
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = fiatAppCurrency,
currencies = currencyList,
)
is Result.Failure -> {
store.dispatchDebugErrorNotification("LoadFiatRate.Failure")
dispatchOnMain(WalletAction.LoadFiatRate.Failure)
}
}
}
@ -293,4 +305,43 @@ class WalletMiddleware {
tokenRate = tokenRate
)
}
private fun checkForRentWarning(walletManager: WalletManager?) {
val rentProvider = walletManager as? RentProvider ?: return
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean {
return balance < rentExempt
}
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
val currency = walletManager.wallet.blockchain.currency
if (show) {
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
))
}
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
}

View file

@ -8,7 +8,9 @@ import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -64,6 +66,9 @@ class WarningsMiddleware {
)
}
}
is WalletAction.Warnings.RestoreFundsWarningClosed -> {
preferencesStorage.saveRestoreFundsWarningClosed()
}
}
}
@ -88,6 +93,9 @@ class WarningsMiddleware {
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
return@let
}
if (card.useOldStyleDerivation && !preferencesStorage.wasRestoreFundsWarningClosed()) {
addWarningMessage(warning = WarningMessagesManager.restoreFundsWarning())
}
showWarningLowRemainingSignaturesIfNeeded(card)
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
@ -193,7 +201,7 @@ class WarningsMiddleware {
}
private fun setWarningMessages() {
store.dispatch(WalletAction.Warnings.Set(getWarnings()))
store.dispatchOnMain(WalletAction.Warnings.Set(getWarnings()))
}
private fun getWarnings(): List<WarningMessage> {

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
@ -14,16 +15,17 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import java.math.BigDecimal
class MultiWalletReducer {
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
val wallets: List<WalletStore> = action.blockchains.map { blockchain ->
val walletManager = action.walletManagers.first {
val wallets: List<WalletStore> = action.blockchains.mapNotNull { blockchain ->
val walletManager = action.walletManagers.firstOrNull {
it.wallet.blockchain == blockchain.blockchain &&
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
}
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
} ?: return@mapNotNull null
val wallet = walletManager.wallet
val cardToken = if (!state.isMultiwalletAllowed) {
wallet.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
@ -99,11 +101,12 @@ class MultiWalletReducer {
addTokens(listOf(action.token), action.blockchain, state)
}
is WalletAction.MultiWallet.TokenLoaded -> {
val pendingTransactions = state.getWalletManager(action.token)
?.wallet?.let { wallet ->
wallet.recentTransactions.toPendingTransactions(wallet.address)
} ?: emptyList()
val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token)
val wallet = state.getWalletManager(currency)?.wallet.guard {
throw NullPointerException("MultiWallet.TokenLoaded: WalletManager must be no NULL")
}
val pendingTransactions = wallet.recentTransactions.toPendingTransactions(wallet.address)
val sendButtonEnabled =
action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val tokenPendingTransactions = pendingTransactions
@ -113,7 +116,7 @@ class MultiWalletReducer {
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(action.token)
val tokenWalletData = state.getWalletData(currency)
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
@ -124,7 +127,8 @@ class MultiWalletReducer {
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
action.amount.value
?.toFiatString(it, store.state.globalState.appCurrency)
}
},
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
@ -150,8 +154,8 @@ class MultiWalletReducer {
is WalletAction.MultiWallet.SetPrimaryToken ->
state.copy(primaryToken = action.token)
is WalletAction.MultiWallet.FindTokensInUse -> state
is WalletAction.MultiWallet.FindBlockchainsInUse -> state
// is WalletAction.MultiWallet.FindTokensInUse -> state
// is WalletAction.MultiWallet.FindBlockchainsInUse -> state
is WalletAction.MultiWallet.SaveCurrencies -> state
}
}
@ -166,11 +170,10 @@ private fun addTokens(
fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? {
if (!state.isMultiwalletAllowed) return null
if (state.currencies.any { it is Currency.Token && it.token == this }) {
return null
}
val currency = Currency.fromBlockchainNetwork(blockchain, this)
if (state.currencies.contains(currency)) return null
val walletManager = state.getWalletManager(this)?.wallet
val walletManager = state.getWalletManager(currency)?.wallet
val walletAddresses = createAddressList(walletManager)
return WalletData(
@ -181,6 +184,6 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = Currency.fromBlockchainNetwork(blockchain, this)
currency = currency
)
}

View file

@ -74,7 +74,8 @@ class OnWalletLoadedReducer {
)
val tokens = wallet.getTokens().mapNotNull { token ->
val tokenWalletData = walletState.getWalletData(token)
val currency = Currency.fromBlockchainNetwork(blockchainNetwork, token)
val tokenWalletData = walletState.getWalletData(currency)
val tokenPendingTransactions =
pendingTransactions.filter { it.currency == token.symbol }
val tokenBalanceStatus = when {

View file

@ -164,7 +164,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
)
}
val wallets = newState.updateTradeCryptoState(exchangeManager, newState.replaceSomeWallets(newWallets))
val wallets = newState.updateTradeCryptoState(
exchangeManager,
newState.replaceSomeWallets(newWallets)
)
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets)
newState = newState.updateWalletStore(walletStore)
}
@ -217,7 +220,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
),
)
val tokenWallets = action.wallet.getTokens()
.mapNotNull { newState.getWalletData(it) }
.mapNotNull { token ->
walletStore?.blockchainNetwork?.let {
newState.getWalletData(Currency.fromBlockchainNetwork(it, token))
}
}
.map {
it.copy(
currencyData = it.currencyData.copy(
@ -288,12 +295,14 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
newState = newState.updateWalletData(selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
newState = newState.updateWalletData(
selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
)
)
))
)
}
is WalletAction.SetWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
@ -305,6 +314,15 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = newState.updateWalletsData(listOf(walletData))
}
}
is WalletAction.RemoveWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData == null) {
newState
} else {
walletData = walletData.copy(warningRent = null)
newState = newState.updateWalletsData(listOf(walletData))
}
}
}
return newState
}

View file

@ -15,6 +15,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.common.extensions.guard
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -24,6 +25,7 @@ import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
@ -54,9 +56,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletState == newState.walletState
}.select { it.walletState }
state.select { it.walletState }
}
}
@ -70,12 +70,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupTransactionsRecyclerView()
setupButtons()
setupTestActionButton()
}
private fun setupTransactionsRecyclerView() = with(binding) {
pendingTransactionAdapter = PendingTransactionsAdapter()
rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext())
@ -93,6 +92,15 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
btnSell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
}
private fun setupTestActionButton() {
view?.findViewById<View>(R.id.l_balance)?.let { view ->
TestActions.initFor(
view = view,
actions = TestWalletDetails.solanaRentExemptWarning()
)
}
}
override fun newState(state: WalletState) {
if (activity == null || view == null) return
if (state.selectedCurrency == null) return
@ -113,11 +121,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(
WalletAction.LoadWallet(
blockchain = BlockchainNetwork(selectedWallet.currency.blockchain, selectedWallet.currency.derivationPath, emptyList())
store.dispatch(WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList()
)
)
))
}
}
@ -165,6 +175,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
private fun handleNotEnoughFundsOnMainCurrency(selectedWalletData: WalletData) = with(binding) {
if (selectedWalletData.currency.isBlockchain()) return@with
if (selectedWalletData.shouldShowCoinAmountWarning()) {
val blockchainName = selectedWalletData.currency.blockchain.fullName
val warningMessage = requireContext().getString(

View file

@ -20,6 +20,8 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
@ -29,6 +31,7 @@ import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
import org.rekotlin.StoreSubscriber
@ -80,6 +83,16 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
setupWarningsRecyclerView()
walletView.changeWalletView(this, binding)
// addCustomActionOnCard()
}
private fun addCustomActionOnCard() {
if (!BuildConfig.TEST_ACTION_ENABLED) return
binding.ivCard.setOnClickListener {
printScanResponseState()
printWalletState()
}
}
private fun setupWarningsRecyclerView() {
@ -151,10 +164,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private fun setupCardImage(cardImage: Artwork?) {
Picasso.get()
.load(cardImage?.artworkId)
.placeholder(R.drawable.card_placeholder_black)
?.error(R.drawable.card_placeholder_black)
?.into(binding.ivCard)
.load(cardImage?.artworkId)
.placeholder(R.drawable.card_placeholder_black)
?.error(R.drawable.card_placeholder_black)
?.into(binding.ivCard)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -166,7 +179,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
CardTou(),
store.state.globalState.appCurrency
store.state.globalState.appCurrency,
tangemTechService = store.state.domainNetworks.tangemTechService
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true

View file

@ -10,6 +10,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.Currency
@ -85,36 +86,42 @@ class WalletAdapter
)
when (wallet.currencyData.status) {
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning()
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning(isCustom)
BalanceStatus.Loading -> {
hideWarning()
hideWarning(isCustom)
if (wallet.currencyData.amountFormatted == null) {
tvExchangeRate.text = root.getString(R.string.wallet_balance_loading)
}
}
BalanceStatus.TransactionInProgress ->
showWarning(root.getString(R.string.wallet_balance_tx_in_progress))
showWarning(root.getString(R.string.wallet_balance_tx_in_progress), isCustom)
BalanceStatus.Unreachable ->
showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable))
showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable), isCustom)
BalanceStatus.NoAccount ->
showWarning(root.getString(R.string.wallet_error_no_account))
showWarning(root.getString(R.string.wallet_error_no_account), isCustom)
else -> {
}
}
}
private fun showWarning(message: String) {
toggleWarning(true)
private fun showWarning(message: String, isCustom: Boolean = false) {
toggleWarning(true, isCustom)
binding.tvStatusErrorMessage.text = message
}
private fun hideWarning() {
toggleWarning(false)
private fun hideWarning(isCustom: Boolean = false) {
toggleWarning(false, isCustom)
}
private fun toggleWarning(show: Boolean) {
binding.tvExchangeRate.show(!show)
private fun toggleWarning(show: Boolean, isCustom: Boolean = false) {
if (!show) {
binding.tvExchangeRate.show(!isCustom)
binding.tvCustomCurrency.show(isCustom)
} else {
binding.tvExchangeRate.hide()
binding.tvCustomCurrency.hide()
}
binding.tvStatusErrorMessage.show(show)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.content.res.Resources
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.ConfigurationCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@ -13,6 +15,7 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.RateCanBeBetterEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
@ -56,7 +59,9 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
tvTitle.text = getString(warning.titleResId, warning.title)
tvMessage.text = getString(
resId = warning.messageResId, default = warning.message, formatArgs = warning.messageFormatArg
resId = warning.messageResId,
default = warning.message,
formatArgs = warning.messageFormatArg
)
}
@ -73,17 +78,34 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> {
binding.groupControlsTemporary.hide()
binding.groupControlsRating.hide()
binding.btnClose.hide()
}
WarningMessage.Type.Temporary -> {
binding.groupControlsRating.hide()
binding.groupControlsTemporary.show()
binding.btnClose.hide()
val buttonAction =
when (warning.titleResId) {
R.string.warning_important_security_info -> {
when {
warning.titleResId == R.string.warning_important_security_info -> {
View.OnClickListener {
store.dispatch(WalletAction.ShowDialog.SignedHashesMultiWalletDialog)
}
}
warning.messageResId == R.string.alert_funds_restoration_message -> {
binding.btnClose.show()
binding.btnClose.setOnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.RestoreFundsWarningClosed)
}
val locale = ConfigurationCompat
.getLocales(Resources.getSystem().configuration)
.get(0)
val url = WarningMessagesManager.getRestoreFundsGuideUrl(locale.language)
View.OnClickListener {
store.dispatchOpenUrl(url)
}
}
else -> {
View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
@ -99,6 +121,7 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
WarningMessage.Type.AppRating -> {
binding.groupControlsTemporary.hide()
binding.groupControlsRating.show()
binding.btnClose.show()
val analyticsHandler = store.state.globalState.analyticsHandlers
binding.btnClose.setOnClickListener {
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)

View file

@ -0,0 +1,84 @@
package com.tangem.tap.features.wallet.ui.test
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.TestAction
import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import java.math.BigDecimal
import kotlin.random.Random
/**
[REDACTED_AUTHOR]
*/
class TestWalletDetails {
companion object {
fun solanaRentExemptWarning(): List<TestAction> {
val checker = SolanaRentWarningActionEmitter()
return listOf(
"BALANCE = 0.0" to { checker.setZeroBalance() },
"BALANCE < 0.00089088" to { checker.setLessThanRentExempt() },
"BALANCE > 0.00089088" to { checker.setMoreThanRentExempt() },
"BALANCE = 0.00089087" to { checker.setLessThanRentExemptByOne() },
"BALANCE = 0.00089088 (rent exempt)" to { checker.setForRentExemptBarrier() },
"BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() },
)
}
}
}
private class SolanaRentWarningActionEmitter {
private val zero = BigDecimal.ZERO
private val one = BigDecimal(0.00000001)
private val rentExemptBarrier = BigDecimal(0.00089088)
private val lessThanRentExemptByOne = rentExemptBarrier.minus(one)
private val moreThanRentExemptByOne = rentExemptBarrier.plus(one)
private val moreThanRentExempt = rentExemptBarrier.plus(Random.nextDouble().toBigDecimal())
private val lessThanRentExempt = rentExemptBarrier
.minus(Random.nextDouble(0.0, rentExemptBarrier.minus(one).toDouble()).toBigDecimal())
fun setZeroBalance() {
setBalance(zero)
}
fun setForRentExemptBarrier() {
setBalance(rentExemptBarrier)
}
fun setLessThanRentExemptByOne() {
setBalance(lessThanRentExemptByOne)
}
fun setMoreThanRentExemptByOne() {
setBalance(moreThanRentExemptByOne)
}
fun setMoreThanRentExempt() {
setBalance(moreThanRentExempt)
}
fun setLessThanRentExempt() {
setBalance(lessThanRentExempt)
}
private fun setBalance(value: BigDecimal) {
val amount = Amount(getBlockchainNetwork().blockchain).copy(value = value)
getWalletManager().apply {
TestActions.testAmountInjectionForWalletManagerEnabled = true
wallet.setAmount(amount)
}
store.dispatch(WalletAction.LoadData)
}
private fun getWalletManager(): WalletManager {
return store.state.walletState.getWalletManager(getBlockchainNetwork())!!
}
private fun getBlockchainNetwork(): BlockchainNetwork {
val currency = store.state.walletState.getSelectedWalletData()!!.currency
return BlockchainNetwork(currency.blockchain, currency.derivationPath, listOf())
}
}

View file

@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
//TODO: refactoring: move to the domain module and aggregate it as the alternative service for TangemTech
class CoinMarketCapService() {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create(getApiKey()) }

View file

@ -0,0 +1,36 @@
package com.tangem.tap.persistence
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.network.api.tangemTech.CurrenciesResponse
/**
[REDACTED_AUTHOR]
*/
class FiatCurrenciesPrefStorage(
private val preferences: SharedPreferences,
private val converter: MoshiJsonConverter,
) {
private val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
private val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
fun migrate() {
preferences.edit(true) {
remove(FIAT_CURRENCIES_KEY_OLD)
}
}
fun save(currencies: List<CurrenciesResponse.Currency>) {
val json: String = converter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
}
fun restore(): List<CurrenciesResponse.Currency> {
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
val type = converter.typedList(CurrenciesResponse.Currency::class.java)
if (json.isNullOrBlank()) return emptyList()
return converter.fromJson(json, type) ?: emptyList()
}
}

View file

@ -4,13 +4,9 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.util.*
@ -20,39 +16,24 @@ class PreferencesStorage(applicationContext: Application) {
val appRatingLaunchObserver: AppRatingLaunchObserver
val usedCardsPrefStorage: UsedCardsPrefStorage
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
init {
incrementLaunchCounter()
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
usedCardsPrefStorage = UsedCardsPrefStorage(preferences)
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
usedCardsPrefStorage.migrate()
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val type = Types.newParameterizedType(List::class.java, FiatCurrency::class.java)
moshi.adapter(type)
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
fiatCurrenciesPrefStorage.migrate()
}
fun getAppCurrency(): FiatCurrencyName {
return preferences.getString(APP_CURRENCY_KEY, DEFAULT_FIAT_CURRENCY)
?: DEFAULT_FIAT_CURRENCY
?: DEFAULT_FIAT_CURRENCY
}
fun saveAppCurrency(fiatCurrencyName: FiatCurrencyName) {
return preferences.edit().putString(APP_CURRENCY_KEY, fiatCurrencyName).apply()
}
fun getFiatCurrencies(): List<FiatCurrency>? {
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
return if (json.isNullOrBlank()) null else fiatCurrenciesAdapter.fromJson(json) as List<FiatCurrency>
}
fun saveFiatCurrencies(currencies: List<FiatCurrency>) {
val json: String = fiatCurrenciesAdapter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
preferences.edit { putString(APP_CURRENCY_KEY, fiatCurrencyName) }
}
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
@ -63,7 +44,7 @@ class PreferencesStorage(applicationContext: Application) {
}
fun saveDisclaimerAccepted() {
preferences.edit().putBoolean(DISCLAIMER_ACCEPTED_KEY, true).apply()
preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) }
}
fun wasDisclaimerAccepted(): Boolean {
@ -71,7 +52,7 @@ class PreferencesStorage(applicationContext: Application) {
}
fun saveTwinsOnboardingShown() {
preferences.edit().putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true).apply()
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
}
fun wasTwinsOnboardingShown(): Boolean {
@ -83,13 +64,21 @@ class PreferencesStorage(applicationContext: Application) {
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
}
fun wasRestoreFundsWarningClosed(): Boolean {
return preferences.getBoolean(RESTORE_FUNDS_CLOSED_KEY, false)
}
fun saveRestoreFundsWarningClosed() {
preferences.edit { putBoolean(RESTORE_FUNDS_CLOSED_KEY, true) }
}
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val RESTORE_FUNDS_CLOSED_KEY = "restoreFundsClosed"
}
}

View file

@ -11,10 +11,9 @@ import timber.log.Timber
*/
class UsedCardsPrefStorage(
private val preferences: SharedPreferences,
private val jsonConverter: MoshiJsonConverter
) {
private val jsonConverter = MoshiJsonConverter.INSTANCE
internal fun migrate() {
val scannedIds = preferences.getString(SCANNED_CARDS_IDS_KEY, null) ?: return
@ -27,7 +26,7 @@ class UsedCardsPrefStorage(
fun scanned(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
?: UsedCardInfo(cardId, true)
?: UsedCardInfo(cardId, true)
save(foundItem, restoredList)
}
@ -39,7 +38,7 @@ class UsedCardsPrefStorage(
fun activationStarted(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
?: UsedCardInfo(cardId, isActivationStarted = true)
?: UsedCardInfo(cardId, isActivationStarted = true)
save(foundItem, restoredList)
}
@ -55,7 +54,7 @@ class UsedCardsPrefStorage(
fun activationFinished(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = false)
?: UsedCardInfo(cardId, isActivationStarted = false)
?: UsedCardInfo(cardId, isActivationStarted = false)
save(foundItem, restoredList)
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1C1C1E"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="16"
android:viewportHeight="19">
<path
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z"
android:fillColor="#1C1C1E"/>
android:fillColor="#1C1C1E"
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" />
</vector>

View file

@ -122,8 +122,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
android:background="@drawable/shape_rectangle_rounded_4"

View file

@ -103,7 +103,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="btn_close, btn_can_be_better, btn_really_cool" />
app:constraint_referenced_ids="btn_can_be_better, btn_really_cool" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
<string name="common_custom">Custom</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
<string name="common_custom">Custom</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
<string name="common_custom">Custom</string>

View file

@ -202,6 +202,7 @@
<string name="token_icon">Значок токена</string>
<string name="add_custom_token_title" translatable="false">Добавить токен</string>
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
<string name="custom_token_creation_error_required_field">Обязательное поле</string>
<string name="custom_token_creation_error_network_not_selected">Пожалуйста, выберите сеть</string>
<string name="custom_token_creation_error_empty_fields">Пожалуйста, заполните все поля</string>
<string name="custom_token_creation_error_wrong_decimals">Количество знаков после запятой должно быть корректным числом не больше %d</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Доступные сети</string>
<string name="alert_manage_tokens_addresses_message">Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства.</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="alert_funds_restoration_message">Если вы совершили ошибку с выбором сети при переводе средств с биржи, эта инструкция поможет вам восстановить средства</string>
<string name="common_custom">Пользовательский</string>

View file

@ -3,21 +3,23 @@
<color name="colorPrimary">#0029FF</color>
<color name="colorSecondary">#0022D4</color>
<color name="accent">#1ACE80</color>
<color name="accent">#19C878</color>
<color name="accent_disabled">#801ACE80</color>
<color name="toolbarColor">@color/backgroundLightGray</color>
<color name="menu_accent_color">@color/accent</color>
<color name="tapButtonColor">@color/accent</color>
<color name="tapButtonDisabled">#661ACE80</color>
<color name="tapButtonDisabled">@color/accent_disabled</color>
<color name="tapButtonColorBlack">@color/darkGray6</color>
<color name="tapButtonBlackDisabled">#6A6A6A</color>
<color name="success">#5AC461</color>
<color name="success">@color/accent</color>
<color name="error">#C4291C</color>
<color name="warning">#FFB71B</color>
<color name="white">#FFFFFF</color>
<color name="toolbarColor">@color/backgroundLightGray</color>
<color name="white">#FFFFFF</color>
<color name="lightGray0">#F3F3F3</color>
<color name="lightGray1">#F8F8FB</color>
<color name="lightGray2">#DADADF</color>
@ -62,6 +64,4 @@
<color name="warning_critical">#CA0F03</color>
<color name="gray_button">#EFEFF0</color>
<color name="menu_accent_color">#18C077</color>
</resources>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
<string name="common_custom">Custom</string>

View file

@ -84,6 +84,7 @@
<string name="add_custom_token_title" translatable="false">Add Token</string>
<string name="custom_token_contract_address_input_title" translatable="false">Contract address</string>
<string name="custom_token_creation_error_required_field" translatable="false">Required field</string>
<string name="custom_token_creation_error_network_not_selected" translatable="false">Please select the network</string>
<string name="custom_token_creation_error_empty_fields" translatable="false">Please fill in all the fields</string>
<string name="custom_token_creation_error_wrong_decimals" translatable="false">Decimal number must be a valid integer, no higher than %d</string>

1
common/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

9
common/build.gradle Normal file
View file

@ -0,0 +1,9 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View file

@ -0,0 +1,8 @@
package com.tangem.common
/**
[REDACTED_AUTHOR]
*/
interface Validator<Data, Error> {
fun validate(data: Data? = null): Error?
}

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